codec-0.2: src/Data/Aeson/Codec.hs
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE FlexibleContexts #-}
module Data.Aeson.Codec
( JSONCodec(..)
, defJSON
-- * JSON object codecs
, ObjectParser, ObjectBuilder, ObjectCodec
, field, field'
, asObject
-- * JSON array codecs
, ArrayParser, ArrayBuilder, ArrayCodec
, element, element'
, asArray
, arrayOf, arrayOf'
) where
import Control.Monad.Codec
import Data.Aeson
import Data.Aeson.Encoding
import qualified Data.Aeson.Encoding.Internal as AEI
import Data.Aeson.Types (Parser, Pair)
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Writer.Strict
import qualified Data.Text as T
import qualified Data.Vector as V
-- | Describes the de/serialization of a type @a@. Equivalent to a `ToJSON` and a `FromJSON` instance.
data JSONCodec a = JSONCodec
{ parseJSONCodec :: Value -> Parser a
, toJSONCodec :: a -> Value
, toEncodingCodec :: a -> Encoding
}
-- | Encode/decode a value with its `ToJSON` and `FromJSON` instances.
defJSON :: (FromJSON a, ToJSON a) => JSONCodec a
defJSON = JSONCodec
{ parseJSONCodec = parseJSON
, toJSONCodec = toJSON
, toEncodingCodec = toEncoding
}
type ObjectParser = ReaderT Object Parser
type ObjectBuilder = Writer ( Series, Endo [ Pair ] )
-- | A codec that parses values out of a given `Object`, and produces
-- key-value pairs into a new one.
type ObjectCodec a = Codec ObjectParser ObjectBuilder a
-- | Store/retrieve a value in a given JSON field, with a given JSONCodec.
field' :: T.Text -> JSONCodec a -> ObjectCodec a
field' key valCodec = Codec
{ codecIn = ReaderT $ \obj -> (obj .: key) >>= parseJSONCodec valCodec
, codecOut = \val ->
writer
( val
, ( pair key (toEncodingCodec valCodec val)
, Endo ((key .= toJSONCodec valCodec val) :)
)
)
}
-- | Store/retrieve a value in a given JSON field, with the default JSON serialization.
field :: (FromJSON a, ToJSON a) => T.Text -> ObjectCodec a
field key = field' key defJSON
-- | Turn an `ObjectCodec` into a `JSONCodec` with an expected name (see `withObject`).
asObject :: String -> ObjectCodec a -> JSONCodec a
asObject err objCodec = JSONCodec
{ parseJSONCodec = withObject err (runReaderT (codecIn objCodec))
, toJSONCodec = object . (`appEndo` []) . snd . execOut
, toEncodingCodec = pairs . fst . execOut
} where execOut = execWriter . codecOut objCodec
type ArrayParser = StateT [ Value ] Parser
type ArrayBuilder = Writer ( Series, [ Value ] )
-- | A codec that serializes data to a sequence of JSON array elements.
type ArrayCodec a = Codec ArrayParser ArrayBuilder a
-- | Expect/append an array element, using a given `JSONCodec`.
element' :: JSONCodec a -> ArrayCodec a
element' valCodec = Codec
{ codecIn = StateT $ \case
[] -> fail "Expected an element, got an empty list."
x : xs -> do
val <- parseJSONCodec valCodec x
return ( val, xs )
, codecOut = \val -> writer ( val, ( AEI.Value $ AEI.retagEncoding $ toEncodingCodec valCodec val, [ toJSONCodec valCodec val ] ) )
}
-- | Expect/append an array element, using the default serialization.
element :: (FromJSON a, ToJSON a) => ArrayCodec a
element = element' defJSON
-- | A codec that parses values out of a given `Array`, and produces
-- key-value pairs into a new one.
asArray :: String -> ArrayCodec a -> JSONCodec a
asArray err arrCodec = JSONCodec
{ parseJSONCodec = withArray err $ \arr -> do
( val, leftover ) <- runStateT (codecIn arrCodec) (V.toList arr)
unless (null leftover) $ fail "Elements left over in parsed array."
return val
, toJSONCodec = Array . V.fromList . snd . execOut
, toEncodingCodec = \val -> case fst (execOut val) of
AEI.Empty -> emptyArray_
AEI.Value enc -> AEI.wrapArray enc
} where execOut = execWriter . codecOut arrCodec
-- | Given a `JSONCodec` for @b@ and a way to turn @a@ into @[ b ]@ and back,
-- create a `JSONCodec` for @a@.
arrayOf' :: (a -> [ b ]) -> ([ b ] -> a) -> JSONCodec b -> JSONCodec a
arrayOf' aToList listToA elemCodec = JSONCodec
{ parseJSONCodec = \arr -> do
vals <- parseJSON arr
parsedVals <- traverse (parseJSONCodec elemCodec) (vals :: [ Value ])
return (listToA parsedVals)
, toJSONCodec = Array . V.fromList . map (toJSONCodec elemCodec) . aToList
, toEncodingCodec = AEI.list (toEncodingCodec elemCodec) . aToList
}
-- | Given a a way to turn @a@ into @[ b ]@ and back, create a `JSONCodec` for @a@.
arrayOf :: (FromJSON b, ToJSON b) => (a -> [ b ]) -> ([ b ] -> a) -> JSONCodec a
arrayOf aToList listToA = arrayOf' aToList listToA defJSON