2

我一直在努力尝试使用 Aeson 来解析 Bitly 的响应。有人可以给我一个关于应该定义哪些 Haskell 类型以及如何使用 Aeson 将以下内容解析为这些类型的提示吗?:

// BITLY EXPAND RESPONSE
{
  "data": {
    "expand": [
      {
        "global_hash": "900913",
        "long_url": "http://google.com/",
        "short_url": "http://bit.ly/ze6poY",
        "user_hash": "ze6poY"
      }
    ]
  },
  "status_code": 200,
  "status_txt": "OK"
}

// BITLY SHORTEN RESPONSE
{
  "data": {
    "global_hash": "900913",
    "hash": "ze6poY",
    "long_url": "http://google.com/",
    "new_hash": 0,
    "url": "http://bit.ly/ze6poY"
  },
  "status_code": 200,
  "status_txt": "OK"
}

这是我到目前为止所尝试的:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}

module BitlyClientResponses where

import           Control.Applicative
import           Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as L (pack)
import qualified Data.HashMap.Strict        as M

data DataStatusCodeStatusTxt =
    DSCST { ddata       :: ResponseData
          , status_code :: Integer
          , status_txt  :: String
          }
    deriving (Eq, Show)

data ResponseData
  = ExpandResponseData { expand :: [Response]
                       }
    deriving (Eq, Show)

data Response = ExpandResponse { long_url    :: String -- URI
                               , global_hash :: String
                               , short_url   :: String -- URI
                               , user_hash   :: String
                               -- , hash        :: [String]
                               -- , error       :: String
                               }
              | J String
              | N String
    deriving (Eq, Show)

instance FromJSON DataStatusCodeStatusTxt where
    parseJSON (Object o) = DSCST <$>
                               o .: "data" <*>
                               o .: "status_code" <*>
                               o .: "status_txt"
    parseJSON x = fail $ "FAIL: DataStatusCodeStatusTxt: " ++ (show x)

instance FromJSON ResponseData where
    parseJSON (Object o) =
        case M.lookup "expand" o of
            -- LOST RIGHT HERE
            Just v  -> return $ ExpandResponseData [J ((show o) ++ " $$$ " ++ (show v))]
            Nothing -> return $ ExpandResponseData [N "N"]
    parseJSON x =  fail $ "FAIL: ResponseData: " ++ (show x)

instance FromJSON Response where
    parseJSON (Object o) = ExpandResponse         <$>
                               o .: "long_url"    <*>
                               o .: "global_hash" <*>
                               o .: "short_url"   <*>
                               o .: "user_hash"
                               -- o .: "hash"        <*>
                               -- o .: "error"       <*>
    parseJSON x =  fail $ "FAIL: Response: " ++ (show x)

parseResponse :: String -> Either String DataStatusCodeStatusTxt
parseResponse x = eitherDecode $ L.pack x

当我输入时(为便于阅读而手工编辑):

"{ \"status_code\": 200,
   \"status_txt\": \"OK\",
   \"data\": { \"expand\": [
                            { \"short_url\": \"http:\\/\\/bit.ly\\/LCJq0b\",
                              \"long_url\": \"http:\\/\\/blog.swisstech.net\\/2012\\/06\\/local-postfix-as-relay-to-amazon-ses.html\",
                              \"user_hash\": \"LCJq0b\",
                              \"global_hash\": \"LCJsVy\" }, ...

我回来了(也手工编辑):

Right
  (Right
    (DSCST
      {ddata = ExpandResponseData {expand = [J "fromList [(\"expand\",Array (fromList [Object fromList [(\"long_url\",String \"http://blog.swisstech.net/2012/06/local-postfix-as-relay-to-amazon-ses.html\"),(\"global_hash\",String \"LCJsVy\"),(\"short_url\",String \"http://bit.ly/LCJq0b\"),(\"user_hash\",String \"LCJq0b\")], ...
$$$
Array (fromList [Object fromList [(\"long_url\",String \"http://blog.swisstech.net/2012/06/local-postfix-as-relay-to-amazon-ses.html\"),(\"global_hash\",String \"LCJsVy\"),(\"short_url\",String \"http://bit.ly/LCJq0b\"),(\"user_hash\",String \"LCJq0b\")], ...

在代码中,查找-- LOST RIGHT HERE. 我不知道如何解析"expand".

很高兴看到如何取得进展。也许我走错了路,有人可以让我直截了当(例如,我到目前为止定义的数据类型可能是关闭的)。

4

1 回答 1

4

有效使用的诀窍AesonparseJSON递归调用。这是在您使用(.:)运算符时隐式完成的,因此看到类似的东西M.lookup通常是一个不好的迹象。我将提供一个简化的示例:(纬度,经度)对的路径,由 JSON 对象的 JSON 数组表示。

data Path  = Path  { points :: [Point] }
data Point = Point { lat :: Double, lon :: Double }

-- JSON format looks a bit like this
--
-- { "points": [ {"latitude": 86, "longitude": 23} ,
--               {"latitude": 0,  "longitude": 16} ,
--               {"latitude": 43, "longitude": 87} ] }

instance FromJSON Path where
  parseJSON = withObject "path" $ \o -> 
    Path <$> o .: "points"

instance FromJSON Point where
  parseJSON = withObject "point" $ \o ->
    Point <$> o .: "latitude"
          <*> o .: "longitude"

从这个片段中可以看出两个要点。首先,请注意使用withObjectto 快速约束Value传递的 toparseJSON被标记为Object- 它与使用模式匹配没有显着不同,但它会产生自动、统一的错误消息,因此值得考虑。

其次,更重要的是,请注意我只定义FromJSON了描述每个对象的高级轮廓的实例。特别是检查身体FromJSON Path

Path <$> o .: "points"

这就是说,我需要查看名为的条目"points"并尝试将其解析为构建 a 所需的任何类型Path——在这种情况下,是一个Points 的列表,[Point]. 这种使用取决于递归定义的FromJSON实例。我们需要解析一个数组,幸好已经存在FromJSON实例

instance FromJSON a => FromJSON [a] where ...

a它被解释为可以解析为任何 JSON 类型的 JSON 数组。在我们的例子a ~ Point中,我们只定义那个实例

instance FromJSON Point where ...

然后递归地依赖于

instance FromJSON Double where ...

这是相当标准的。


您可以使用的另一个重要技巧是将多个解析与(<|>). 我将Response稍微简化数据类型,它要么解析为特定类型,要么Object失败并生成一个普通的、动态类型Value的默认值。首先,我们将独立编写每个解析器。

data Obj = Obj { foo :: String, bar :: String }
         | Dyn Value

okParse :: Value -> Parser Obj
okParse = withObject "obj" (\o -> Obj <$> o .: "foo" <*> o .: "bar")

elseParse :: Value -> Parser Obj
elseParse v = pure (Dyn v)

现在我们在实际FromJSON实例中结合它们

instance FromJSON Obj where
  parseJSON v = okParse v <|> elseParse v

在这种情况下,aeson将尝试先使用okParse,如果失败,则重新使用elseParse. 由于elseParse只是一个pure值,它永远不会失败,因此提供了“默认”回退。

于 2014-03-19T18:30:37.913 回答