用已知变量字段解析 JSON
Parse JSON with known variable field
我有一个 Haskell query
函数可以使用
获取最新的代币价格
https://coinmarketcap.com/api/documentation/v1/#operation/getV1CryptocurrencyQuotesLatest
该函数将代币 id 作为 arg,对于 ADA 来说 2010
。
import Data.Aeson
import Network.HTTP.Req
newtype Rate = Rate Double
query :: Int -> IO (Either Text Rate)
query tokenId =
let
url = https queryPrefix /: "v1" /: "cryptocurrency" /: "quotes" /: "latest"
idParam = "id" =: tokenId
options = standardHeader <> idParam
in
runReq defaultHttpConfig $ do
v <- req GET url NoReqBody jsonResponse options
let responseCode = responseStatusCode v
if isValidHttpResponse responseCode then do
case fromJSON $ responseBody v of
Success x -> pure $ Right x
Error e -> pure $ Left $ pack $ "Error decoding state: " <> e
else
pure $ Left $ pack ("Error with CoinMarketCap query 'Quotes Latest': " <> show responseCode <> ". " <> show (responseStatusMessage v))
Json 输出虽然以“2010”为键:
{"status":
{"timestamp":"2021-10-24T03:35:01.583Z","error_code":0,"error_message":null,"elapsed":163,"credit_count":1,"notice":null}
,"data":
{"2010":
{"id":2010
,"name":"Cardano"
,"symbol":"ADA"
,"slug":"cardano"
,"num_market_pairs":302,"date_added":"2017-10-01T00:00:00.000Z"
,"tags":["mineable","dpos","pos","platform","research","smart-contracts","staking","binance-smart-chain","cardano-ecosystem"]
,"max_supply":45000000000
,"circulating_supply":32904527668.666
,"total_supply":33250650235.236,"is_active":1
,"platform":null
,"cmc_rank":4
,"is_fiat":0
,"last_updated":"2021-10-24T03:33:31.000Z"
,"quote":
{"USD":
{"price":2.16109553945978
,"volume_24h":2048006882.386299
,"volume_change_24h":-24.06,"percent_change_1h":0.24896227
,"percent_change_24h":0.38920394
,"percent_change_7d":-0.97094597
,"percent_change_30d":-6.13245906
,"percent_change_60d":-21.94246757
,"percent_change_90d":63.56901345
,"market_cap":71109827972.785
,"market_cap_dominance":2.7813
,"fully_diluted_market_cap":97249299275.69,"last_updated":"2021-10-24T03:33:31.000Z"}}}}}
由于 2010
是 query
的参数,我显然不想像 data.2010.quote.USD.price
那样深入研究:
instance FromJSON Rate where
parseJSON = withObject "Rate" $ \o -> do
dataO <- o .: "data"
_2010O <- dataO .: "2010" -- #############
quoteO <- _2010O .: "quote"
usdO <- quoteO .: "USD"
price <- usdO .: "price"
pure $ Rate price
问:我怎样才能达到我想要的灵活性?我能以某种方式将令牌 ID 传递给 parseJSON
吗?或者是否有使用通配符的 Lens-Aeson 技术? ...
你完全确定 "data"
中的对象只会有一个键,我们可以获取对象,将其转换为值列表,如果列表为空或有更多值则失败大于一个值,否则继续解析。像这样:
instance FromJSON Rate where
parseJSON = withObject "Rate" $ \o -> do
Object dataO <- o .: "data" -- we expect an Object
-- get the single value, it should be an Object itself
[Object _2010O] <- pure $ Data.Foldable.toList dataO
quoteO <- _2010O .: "quote"
usdO <- quoteO .: "USD"
price <- usdO .: "price"
pure $ Rate price
当没有键、多个键或值不是 aeson Object
时,模式 [Object _2010O] <-
无法匹配并通过 MonadFail
给出解析错误aeson 的实例 Parser
.
我们还可以更明确一点:
instance FromJSON Rate where
parseJSON = withObject "Rate" $ \o -> do
Object dataO <- o .: "data"
let objects = Data.Foldable.toList dataO
case objects of
[Object _2010O] -> do
quoteO <- _2010O .: "quote"
usdO <- quoteO .: "USD"
price <- usdO .: "price"
pure $ Rate price
[_] -> fail "value is not Object"
_ -> fail "zero or more than one key"
it seems a pity that being that I know the key name upfront ("2010" in
the example), I do not use that info when parsing
问题在于,类型类方法除了它们自己的参数外,只能访问编译时已知的 static 信息。并且 tokenId
很可能是运行时信息,例如从配置文件中读取。
因此,一种解决方案可能涉及减少对 FromJSON
实例的依赖。与其直接解析 Rate
,不如先解析为 Value
(Aeson 的 Value
有一个 FromJSON
实例),然后将 Value
解析为 Rate
parsing 在函数 外部 FromJSON
类型类中,一个在范围内具有 tokenId
的函数。
不过,假设我们想最大程度地依赖 FromJSON
个实例。我们可以尝试“return 一个接受我们仍然不知道的数据的函数”的技巧,通过定义一个像
这样的辅助新类型
-- we need to pass the tokenId to get the to the Rate
newtype RateWoTokenId = RateWoTokenId (Text -> Result Rate)
还有一个 FromJSON
实例,例如
instance FromJSON RateWoTokenId where
parseJSON = withObject "Rate" $ \o -> do
dataO <- o .: "data"
pure $ RateWoTokenId $ \tokenId -> -- returning a function here!
-- We continue parsing inside the function,
-- because the tokenId is known there.
flip Data.Aeson.Types.parse dataO $ \dataO -> do
_2010O <- dataO .: Data.Aeson.Key.fromText tokenId
quoteO <- _2010O .: "quote"
usdO <- quoteO .: "USD"
price <- usdO .: "price"
pure $ Rate price
我有一个 Haskell query
函数可以使用
https://coinmarketcap.com/api/documentation/v1/#operation/getV1CryptocurrencyQuotesLatest
该函数将代币 id 作为 arg,对于 ADA 来说 2010
。
import Data.Aeson
import Network.HTTP.Req
newtype Rate = Rate Double
query :: Int -> IO (Either Text Rate)
query tokenId =
let
url = https queryPrefix /: "v1" /: "cryptocurrency" /: "quotes" /: "latest"
idParam = "id" =: tokenId
options = standardHeader <> idParam
in
runReq defaultHttpConfig $ do
v <- req GET url NoReqBody jsonResponse options
let responseCode = responseStatusCode v
if isValidHttpResponse responseCode then do
case fromJSON $ responseBody v of
Success x -> pure $ Right x
Error e -> pure $ Left $ pack $ "Error decoding state: " <> e
else
pure $ Left $ pack ("Error with CoinMarketCap query 'Quotes Latest': " <> show responseCode <> ". " <> show (responseStatusMessage v))
Json 输出虽然以“2010”为键:
{"status":
{"timestamp":"2021-10-24T03:35:01.583Z","error_code":0,"error_message":null,"elapsed":163,"credit_count":1,"notice":null}
,"data":
{"2010":
{"id":2010
,"name":"Cardano"
,"symbol":"ADA"
,"slug":"cardano"
,"num_market_pairs":302,"date_added":"2017-10-01T00:00:00.000Z"
,"tags":["mineable","dpos","pos","platform","research","smart-contracts","staking","binance-smart-chain","cardano-ecosystem"]
,"max_supply":45000000000
,"circulating_supply":32904527668.666
,"total_supply":33250650235.236,"is_active":1
,"platform":null
,"cmc_rank":4
,"is_fiat":0
,"last_updated":"2021-10-24T03:33:31.000Z"
,"quote":
{"USD":
{"price":2.16109553945978
,"volume_24h":2048006882.386299
,"volume_change_24h":-24.06,"percent_change_1h":0.24896227
,"percent_change_24h":0.38920394
,"percent_change_7d":-0.97094597
,"percent_change_30d":-6.13245906
,"percent_change_60d":-21.94246757
,"percent_change_90d":63.56901345
,"market_cap":71109827972.785
,"market_cap_dominance":2.7813
,"fully_diluted_market_cap":97249299275.69,"last_updated":"2021-10-24T03:33:31.000Z"}}}}}
由于 2010
是 query
的参数,我显然不想像 data.2010.quote.USD.price
那样深入研究:
instance FromJSON Rate where
parseJSON = withObject "Rate" $ \o -> do
dataO <- o .: "data"
_2010O <- dataO .: "2010" -- #############
quoteO <- _2010O .: "quote"
usdO <- quoteO .: "USD"
price <- usdO .: "price"
pure $ Rate price
问:我怎样才能达到我想要的灵活性?我能以某种方式将令牌 ID 传递给 parseJSON
吗?或者是否有使用通配符的 Lens-Aeson 技术? ...
你完全确定 "data"
中的对象只会有一个键,我们可以获取对象,将其转换为值列表,如果列表为空或有更多值则失败大于一个值,否则继续解析。像这样:
instance FromJSON Rate where
parseJSON = withObject "Rate" $ \o -> do
Object dataO <- o .: "data" -- we expect an Object
-- get the single value, it should be an Object itself
[Object _2010O] <- pure $ Data.Foldable.toList dataO
quoteO <- _2010O .: "quote"
usdO <- quoteO .: "USD"
price <- usdO .: "price"
pure $ Rate price
当没有键、多个键或值不是 aeson Object
时,模式 [Object _2010O] <-
无法匹配并通过 MonadFail
给出解析错误aeson 的实例 Parser
.
我们还可以更明确一点:
instance FromJSON Rate where
parseJSON = withObject "Rate" $ \o -> do
Object dataO <- o .: "data"
let objects = Data.Foldable.toList dataO
case objects of
[Object _2010O] -> do
quoteO <- _2010O .: "quote"
usdO <- quoteO .: "USD"
price <- usdO .: "price"
pure $ Rate price
[_] -> fail "value is not Object"
_ -> fail "zero or more than one key"
it seems a pity that being that I know the key name upfront ("2010" in the example), I do not use that info when parsing
问题在于,类型类方法除了它们自己的参数外,只能访问编译时已知的 static 信息。并且 tokenId
很可能是运行时信息,例如从配置文件中读取。
因此,一种解决方案可能涉及减少对 FromJSON
实例的依赖。与其直接解析 Rate
,不如先解析为 Value
(Aeson 的 Value
有一个 FromJSON
实例),然后将 Value
解析为 Rate
parsing 在函数 外部 FromJSON
类型类中,一个在范围内具有 tokenId
的函数。
不过,假设我们想最大程度地依赖 FromJSON
个实例。我们可以尝试“return 一个接受我们仍然不知道的数据的函数”的技巧,通过定义一个像
-- we need to pass the tokenId to get the to the Rate
newtype RateWoTokenId = RateWoTokenId (Text -> Result Rate)
还有一个 FromJSON
实例,例如
instance FromJSON RateWoTokenId where
parseJSON = withObject "Rate" $ \o -> do
dataO <- o .: "data"
pure $ RateWoTokenId $ \tokenId -> -- returning a function here!
-- We continue parsing inside the function,
-- because the tokenId is known there.
flip Data.Aeson.Types.parse dataO $ \dataO -> do
_2010O <- dataO .: Data.Aeson.Key.fromText tokenId
quoteO <- _2010O .: "quote"
usdO <- quoteO .: "USD"
price <- usdO .: "price"
pure $ Rate price