在 Haskell 中递归 JSON 解析

Recursive JSON parsing in Haskell

我有以下 haskell 代码。虽然太长了,复制粘贴一下就可以了:

module DebugVersionJSON where

import Data.Attoparsec.Char8
import qualified Data.Aeson as JSON
import qualified Data.Text as T
import qualified Data.ByteString.Char8 as BS
import Control.Applicative
import Control.Monad
import qualified Data.HashMap.Strict as HashMap

data VersionCompound = NumberPlaceholder                      -- X
                     | Number Int                           -- 1, 2, 3, ..., 45, ... 
                     deriving (Show)

instance Eq VersionCompound where
    NumberPlaceholder == NumberPlaceholder = True
    (Number v1) == (Number v2)            = (v1 == v2)
    _ == _                                 = False

type NumberOfDimensions = VersionCompound

versionCompoundToString :: VersionCompound -> String
versionCompoundToString (Number n) = (show n)
versionCompoundToString NumberPlaceholder = "x"

parseVersionCompound :: Parser VersionCompound
parseVersionCompound =
     ( string (BS.pack "x")    >> return NumberPlaceholder)
 <|> ( string (BS.pack "X")    >> return NumberPlaceholder)
 <|> ( decimal >>= \num -> return (Number num) )

data VersionNumber = VersionCompound VersionCompound
                    | VersionNumber VersionCompound VersionNumber
                    deriving (Show)

instance Eq VersionNumber where
    ( VersionCompound vc1 ) == ( VersionCompound vc2 ) = (vc1 == vc2)
    ( VersionNumber vc1 vn1 ) == ( VersionNumber vc2 vn2 ) = (vc1 == vc2 && vn1 == vn2)
    ( VersionNumber vc1 vn1 ) == ( VersionCompound vc2 ) = (vc1 == vc2 && vn1 == (VersionCompound NumberPlaceholder) ) 
    ( VersionCompound vc1 ) == ( VersionNumber vc2 vn2) = (vc1 == vc2 && vn2 == (VersionCompound NumberPlaceholder) )

versionNumberToString :: VersionNumber -> String
versionNumberToString (VersionNumber vc vn) = (versionCompoundToString vc) ++ "." ++ (versionNumberToString vn)
versionNumberToString (VersionCompound vc) = (versionCompoundToString vc)

parseVersionNumber :: Parser VersionNumber
parseVersionNumber = do
    ds <- sepBy1 parseVersionCompound (char '.')
    let vs = map VersionCompound ds
    return (foldr1 (\(VersionCompound vc) -> VersionNumber vc) vs )

data MaturityLevel = Dev
                   | Test
                   | User
                   | ReleaseCandidate
                   | Prod
                   deriving (Show, Enum, Ord, Eq)

parseMaturity :: Parser MaturityLevel
parseMaturity = 
        ( string (BS.pack "Dev") >> return Dev)
    <|> ( string (BS.pack "Test") >> return Test)
    <|> ( string (BS.pack "User") >> return User)
    <|> ( string (BS.pack "ReleaseCandidate") >> return ReleaseCandidate)
    <|> ( string (BS.pack "Prod") >> return Prod)

data Version = MaturityVersion MaturityLevel VersionNumber  -- Dev/1.x.0, Test/1.x.3, User/1.x.4, User/2.5.1, ...
            | Version VersionNumber

instance Show Version where
    show version = versionToString version

instance Eq Version where
    (Version vn1) == (Version vn2) = (vn1 == vn2)
    (Version vn1) == (MaturityVersion ml vn2) = (ml == Dev) && vn1 == vn2
    (MaturityVersion ml vn1) == (Version vn2) = (ml == Dev) && vn1 == vn2
    (MaturityVersion ml1 vn1) == (MaturityVersion ml2 vn2)      = ( ml1 == ml2 ) && (vn1 == vn2)

versionToString :: Version -> String
versionToString (MaturityVersion maturityLevel versionNumber) = (show maturityLevel) ++ "/" ++ (versionNumberToString versionNumber)
versionToString (Version versionNumber) = (versionNumberToString versionNumber)

instance JSON.ToJSON Version where
    toJSON version = 
        JSON.object [ T.pack "version" JSON..= (T.pack $ show version)]

instance JSON.FromJSON Version where
    parseJSON (JSON.Object v) = liftM stringToVersion ( v JSON..: T.pack "version" )
    parseJSON _ = mzero

parseVersion :: Parser Version
parseVersion = do { 
        maturity <- parseMaturity
      ; char '/'
      ; version <- parseVersionNumber
      ; return $ MaturityVersion maturity version
    } 
    <|> do { 
        version <- parseVersionNumber
      ; return $ Version version
    } 

class VersionOperations a where
    decrement :: a -> a
    decrementDimension :: NumberOfDimensions -> a -> a
    increment :: a -> a
    incrementDimension :: NumberOfDimensions -> a -> a

instance VersionOperations VersionCompound where 
    decrement           NumberPlaceholder       = NumberPlaceholder
    decrement           (Number 0)              = Number 0
    decrement           (Number num)            = Number (num - 1)
    decrementDimension  _                   a   = decrement a
    increment           NumberPlaceholder       = NumberPlaceholder
    increment           (Number num)            = Number (num + 1)
    incrementDimension  _                   a   = increment a

createVersionNumberByNumberOfDimensions :: NumberOfDimensions -> VersionNumber
createVersionNumberByNumberOfDimensions ( NumberPlaceholder ) = VersionCompound NumberPlaceholder
createVersionNumberByNumberOfDimensions ( Number 0 ) = VersionCompound NumberPlaceholder
createVersionNumberByNumberOfDimensions ( Number 1 ) = VersionCompound NumberPlaceholder
createVersionNumberByNumberOfDimensions num = VersionNumber NumberPlaceholder ( createVersionNumberByNumberOfDimensions ( decrement num ) )

stringToVersion :: String -> Version
stringToVersion str = case (parseOnly parseVersion $ BS.pack str) of
    Right a -> a
    Left _ -> Version ( createVersionNumberByNumberOfDimensions (Number 0) )


vc1 :: VersionCompound
vc1 = NumberPlaceholder

vc2 :: VersionCompound
vc2 = (Number 1)

vc3 :: VersionCompound
vc3 = (Number 2)

v4 :: Version
v4 = MaturityVersion Dev ( VersionCompound ( Number 3 ) )

v5 :: Version
v5 = MaturityVersion ReleaseCandidate ( VersionCompound ( Number 50 ) )


type DocumentName = String
type DirectoryName = String
type DocumentContent = String

data Document = Document DocumentName DocumentContent deriving (Show, Eq)
data Directory = Directory DirectoryName [DocumentOrDirectory] deriving (Show, Eq)
newtype DocumentOrDirectory = DocumentOrDirectory (Either Document Directory) deriving (Show, Eq)

emptyDocument = ( Document "" "" )

-- instance Show DocumentOrDirectory where
    -- show (Document name content ) = "Document: " ++ name ++ ", Content: " ++ content ++ ""
    -- show (Directory dirName content ) = "Directory: " ++ dirName ++ ", Content: " ++ (show content) ++ ""

liftDocument :: Document -> DocumentOrDirectory
liftDocument = DocumentOrDirectory . Left

liftDirectory :: Directory -> DocumentOrDirectory
liftDirectory = DocumentOrDirectory . Right


-- ToJSON
instance JSON.ToJSON Document where
  toJSON (Document name content) = JSON.object [ T.pack "document" JSON..= JSON.object [
    T.pack "name"    JSON..= name,
    T.pack "content" JSON..= content ]]

instance JSON.ToJSON Directory where
  toJSON (Directory name content) = JSON.object [ T.pack "directory" JSON..= JSON.object [
    T.pack "name"    JSON..= name,
    T.pack "content" JSON..= content ]]

instance JSON.ToJSON DocumentOrDirectory where
  toJSON (DocumentOrDirectory (Left d))  = JSON.toJSON d
  toJSON (DocumentOrDirectory (Right d)) = JSON.toJSON d

-- FromJSON 
instance JSON.FromJSON Document where
  parseJSON (JSON.Object v) = maybe mzero parser $ HashMap.lookup (T.pack "document") v
    where parser (JSON.Object v') = Document <$> v' JSON..: T.pack "name"
                                        <*> v' JSON..: T.pack "content"
          parser _           = mzero
  parseJSON _          = mzero

instance JSON.FromJSON Directory where
  parseJSON (JSON.Object v) = maybe mzero parser $ HashMap.lookup (T.pack "directory") v
    where parser (JSON.Object v') = Directory <$> v' JSON..: T.pack "name"
                                         <*> v' JSON..: T.pack "content"
          parser _           = mzero
  parseJSON _          = mzero

instance JSON.FromJSON DocumentOrDirectory where
  parseJSON json = (liftDocument <$> JSON.parseJSON json) <|> (liftDirectory <$> JSON.parseJSON json)

-- EXAMPLES --
doc1 :: Document
doc1 = Document "doc1" "content1"
doc2 :: Document
doc2 = Document "doc2" "content2"


type BranchName = String
type Timestamp = Integer

data Snapshot = Snapshot Timestamp Version DocumentOrDirectory 
data Snapshot2 = Snapshot2 Timestamp DocumentOrDirectory deriving (Show, Eq)

instance Eq Snapshot where
    (Snapshot timestampA versionA _ ) == (Snapshot timestampB versionB _ )  = (timestampA == timestampB) && (versionA == versionB)
    _ == _                                                                  = False

instance Show Snapshot where
    show (Snapshot timestamp version contents ) = ("Snapshot taken at " ++ (show timestamp) ++ ", Version " ++ (versionToString version) ++ ", " ++ (show contents) ++ "")

instance JSON.ToJSON Snapshot where        
    toJSON (Snapshot timestamp version document) = JSON.object [ T.pack "snapshot" JSON..= JSON.object [ 
      T.pack "version"  JSON..= JSON.toJSON version,
      T.pack "timestamp" JSON..= timestamp,
      T.pack "artifact"  JSON..= JSON.toJSON document ]]

instance JSON.ToJSON Snapshot2 where        
    toJSON (Snapshot2 timestamp document) = JSON.object [ T.pack "snapshot" JSON..= JSON.object [ 
      T.pack "timestamp" JSON..= timestamp,
      T.pack "artifact"  JSON..= JSON.toJSON document ]]

instance JSON.FromJSON Snapshot where
  parseJSON (JSON.Object v) = maybe mzero parser $ HashMap.lookup (T.pack "snapshot") v
    where parser (JSON.Object v') = Snapshot <$> v' JSON..: T.pack "version"
                                             <*> v' JSON..: T.pack "timestamp"
                                             <*> v' JSON..: T.pack "artifact"
          parser _                = mzero
  parseJSON _          = mzero

instance JSON.FromJSON Snapshot2 where
  parseJSON (JSON.Object v) = maybe mzero parser $ HashMap.lookup (T.pack "snapshot") v
    where parser (JSON.Object v') = Snapshot2 <$> v' JSON..: T.pack "timestamp"
                                             <*> v' JSON..: T.pack "artifact"
          parser _                = mzero
  parseJSON _          = mzero



snapshot1 :: Snapshot
snapshot1 = Snapshot 12372 ( MaturityVersion Dev ( VersionCompound ( Number 10 ) ) ) ( liftDocument doc1 )

snapshot2 :: Snapshot2
snapshot2 = Snapshot2 12372  ( liftDocument doc1 )

一方面,JSON.decode $ JSON.encode snapshot2 :: Maybe Snapshot2 执行得很好,结果是 Just (Snapshot2 12372 (DocumentOrDirectory (Left (Document "doc1" "content1"))))。另一方面,JSON.decode $ JSON.encode snapshot :: Maybe Snapshot 导致 Nothing

两个解析器的区别如下:

instance JSON.ToJSON Snapshot where        
    toJSON (Snapshot timestamp version document) = JSON.object [ T.pack "snapshot" JSON..= JSON.object [ 
      T.pack "version"  JSON..= JSON.toJSON version, -- <- includes version parsing
      T.pack "timestamp" JSON..= timestamp,
      T.pack "artifact"  JSON..= JSON.toJSON document ]]

instance JSON.ToJSON Snapshot2 where        
    toJSON (Snapshot2 timestamp document) = JSON.object [ T.pack "snapshot" JSON..= JSON.object [ 
      T.pack "timestamp" JSON..= timestamp,
      T.pack "artifact"  JSON..= JSON.toJSON document ]]

知道为什么 JSON.decode $ JSON.encode snapshot :: Maybe Snapshot 失败了吗?我知道版本解析有问题,但我不知道到底是什么。如果您能帮我弄清楚如何修复版本解析,以便我可以正确解析 JSON,我会很高兴。

问题是您将快照定义为:

data Snapshot = Snapshot Timestamp Version DocumentOrDirectory

但是您的 FromJSON 实例是:

where parser (JSON.Object v') = Snapshot <$> v' JSON..: T.pack "version"
                                         <*> v' JSON..: T.pack "timestamp"
                                         <*> v' JSON..: T.pack "artifact"

即- 您混淆了字段的顺序。