简体   繁体   English

使用已知变量字段解析 JSON

[英]Parse JSON with known variable field

I have a Haskell query function to get latest token price using我有一个 Haskell query函数来获取最新的代币价格

https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest

The function takes token id as arg, say 2010 for ADA.该函数将令牌 ID 作为参数,例如 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))              

The Json output though has "2010" as a key: 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"}}}}}

Being that 2010 is an arg to query , I clearly do not want to drill in as data.2010.quote.USD.price with something like this:由于2010query的 arg,我显然不想像data.2010.quote.USD.price那样钻取如下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  

Question: How can I achieve the flexibility I want?问题:我怎样才能达到我想要的灵活性? Can I somehow pass in the token id to parseJSON ?我可以以某种方式将令牌 ID 传递给parseJSON吗? Or is there perhaps a Lens-Aeson technique to use a wildcard?或者是否有使用通配符的 Lens-Aeson 技术? ... ...

I you are completely sure that the object inside "data" will only ever have a single key, we can take the object, convert it into a list of values, fail if the list is empty or has more than one value, and otherwise continue parsing.我完全确定"data"中的对象永远只有一个键,我们可以获取该对象,将其转换为值列表,如果列表为空或具有多个值则失败,否则继续解析。 Like this:像这样:

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 

When there's no key, more than one key, or the value is not an aeson Object , the pattern [Object _2010O] <- fails to match and gives an parsing error through the MonadFail instance of aeson's Parser .当没有键、多个键或值不是[Object _2010O] <- Object ,模式[Object _2010O] <-无法匹配并通过MonadFailParserMonadFail实例给出解析错误。

We could also be a bit more explicit:我们也可以更明确一点:

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可惜我预先知道键名(示例中的“2010”),我在解析时不使用该信息

The problem is that typeclass methods, apart from their own arguments, only have access to static information known at compile time.问题是类型类方法,除了它们自己的参数,只能访问编译时已知的静态信息。 An the tokenId is likely to be runtime information, for example read from a configuration file. tokenId可能是运行时信息,例如从配置文件中读取。

Therefore, one solution could involve relying a bit less on the FromJSON instance.因此,一种解决方案可能涉及减少对FromJSON实例的依赖。 Instead of parsing Rate directly, parse to a Value first (Aeson's Value has a FromJSON instance) and then do the Value to Rate parsing in a function outside the FromJSON typeclass, a function that has the tokenId in scope.不是直接解析Rate ,而是首先解析到一个Value (Aeson 的Value有一个FromJSON实例),然后在FromJSON类型类之外的函数中进行Value to Rate 解析,该函数在范围内具有tokenId


Still, suppose we want to rely on FromJSON instances to the greatest degree possible.不过,假设我们希望最大程度地依赖FromJSON实例。 We could try the "return a function that accepts the data we still don't know" trick, by defining a helper newtype like我们可以尝试“返回一个接受我们仍然不知道的数据的函数”的技巧,通过定义一个像这样的帮助器 newtype

-- we need to pass the tokenId to get the to the Rate
newtype RateWoTokenId = RateWoTokenId (Text -> Result Rate) 

And a FromJSON instance like和一个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          

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM