Вот мой план: при разборе мы будем отслеживать, какие ключи мы просмотрели. Парсеры, которые не используют все ключи объекта, потерпят неудачу. Вот ваша преамбула, выделенная для полноты и компиляции:
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Applicative
import Control.Monad.Writer
import Data.Aeson
import Data.Aeson.Types (Parser)
import Data.Functor.Compose
import Data.HashMap.Lazy (HashMap)
import Data.Text (Text)
import qualified Data.HashMap.Lazy as HM
data VersionedThing = V1 Thing1 | V2 Thing2 deriving (Eq, Ord, Read, Show)
data Thing1 = Thing1
{ name :: Maybe String
, val1 :: Maybe String
, val2 :: Maybe String
} deriving (Eq, Ord, Read, Show)
data Thing2 = Thing2
{ name :: Maybe String
, val3 :: Maybe String
, val4 :: Maybe String
} deriving (Eq, Ord, Read, Show)
Теперь мы добавим тип для разбора и отслеживания одновременно, а также встраивания для «просто разбора без отслеживания» и «просто отслеживания без разбора».
type ParseAndTrack = Compose Parser (Writer (HashMap Text ()))
parse :: Parser a -> ParseAndTrack a
track :: Text -> ParseAndTrack ()
parse p = Compose (pure <$> p)
track t = Compose . pure . tell $ HM.singleton t ()
Мы можем использовать эти два примитива для поднятия (.:)
и (.:?)
к отслеживаемым версиям самих себя. Мы будем использовать суффикс &
для вещей, которые анализируют и отслеживают.
(.:&) :: FromJSON a => Object -> Text -> ParseAndTrack a
o .:& t = track t *> parse (o .: t)
(.:?&) :: FromJSON a => Object -> Text -> ParseAndTrack (Maybe a)
o .:?& t = (Just <$> (o .:& t)) <|> pure Nothing
Наконец, мы дадим высокоуровневый способ возврата из режима «разбора и отслеживания» в режим «только разбора», если произойдет сбой, если мы не использовали все доступные ключи.
consumeAllOf :: Object -> ParseAndTrack a -> Parser a
consumeAllOf o p = do
(result, keys) <- runWriter <$> getCompose p
let unusedKeys = HM.difference o keys
unless (null unusedKeys) . fail $
"unrecognized keys " ++ show (HM.keys unusedKeys)
return result
Теперь мы можем написать два ваших синтаксических анализатора с помощью вышеперечисленных дополнительных инструментов, и все должно просто работать. Единственная разница в синтаксических анализаторах для Thing1
и Thing2
заключается в том, что мы добавляем consumeAllOf
впереди и используем отслеживающие версии .:
и .:?
в середине.
instance FromJSON Thing1 where
parseJSON (Object v) = consumeAllOf v $ Thing1 <$> v.:& "name" <*> v.:?& "val1" <*> v .:?& "val2"
instance FromJSON Thing2 where
parseJSON (Object v) = consumeAllOf v $ Thing2 <$> v.:& "name" <*> v.:?& "val3" <*> v .:?& "val4"
instance FromJSON (VersionedThing) where
parseJSON v = (V1 <$> parseJSON v)
`mplus` (V2 <$> parseJSON v)
Попробуйте в ghci:
> decode "{\"name\": \"foo\", \"val1\": \"bar\"}" :: Maybe VersionedThing
Just (V1 (Thing1 {name = Just "foo", val1 = Just "bar", val2 = Nothing}))
> decode "{\"name\": \"foo\", \"val3\": \"bar\"}" :: Maybe VersionedThing
Just (V2 (Thing2 {name = Just "foo", val3 = Just "bar", val4 = Nothing}))