Codec makes it simple to write composable bidirectional serializers with a consistent interface.
Just define your data type normally:
data RecordB = RecordB { recordBString :: String , recordBDouble :: Double } deriving (Eq, Ord, Show)
and then associate each field with a codec using the =. operator:
recordBObjCodec :: JSONCodec RecordB recordBObjCodec = asObject "RecordB" $ RecordB <$> recordBString =. field "string" <*> recordBDouble =. field "double"
That's it! If you want, you can now define ToJSON and FromJSON instances, or just use it directly:
instance ToJSON RecordB where toJSON = toJSONCodec recordBObjCodec toEncoding = toEncodingCodec recordBObjCodec instance FromJSON RecordB where parseJSON = parseJSONCodec recordBObjCodec
Support can be added for almost any serialization library, but aeson and binary support are included.
JSON example:
data RecordA = RecordA { recordAInt :: Int , recordANestedObj :: RecordB , recordANestedArr :: RecordB , recordANestedObjs :: [ RecordB ] } deriving (Eq, Ord, Show) data RecordB = RecordB { recordBString :: String , recordBDouble :: Double } deriving (Eq, Ord, Show) recordACodec :: JSONCodec RecordA recordACodec = asObject "RecordA" $ RecordA <$> recordAInt =. field "int" <*> recordANestedObj =. field' "nestedObj" recordBObjCodec <*> recordANestedArr =. field' "nestedArr" recordBArrCodec <*> recordANestedObjs =. field' "nestedObjs" (arrayOf' id id recordBObjCodec) recordBObjCodec :: JSONCodec RecordB recordBObjCodec = asObject "RecordB" $ RecordB <$> recordBString =. field "string" <*> recordBDouble =. field "double" -- serialize to array elements recordBArrCodec :: JSONCodec RecordB recordBArrCodec = asArray "RecordB" $ RecordB <$> recordBString =. element <*> recordBDouble =. element
Binary example:
data RecordA = RecordA { recordAInt64 :: Int64 , recordAWord8 :: Word8 , recordANestedB :: RecordB } deriving (Eq, Ord, Show) data RecordB = RecordB { recordBWord16 :: Word16 , recordBByteString64 :: BS.ByteString } deriving (Eq, Ord, Show) recordACodec :: BinaryCodec RecordA recordACodec = RecordA <$> recordAInt64 =. int64le <*> recordAWord8 =. word8 <*> recordANestedB =. recordBCodec recordBCodec :: BinaryCodec RecordB recordBCodec = RecordB <$> recordBWord16 =. word16host <*> recordBByteString64 =. byteString 64
A Codec is just a combination of a deserializer r a, and a serializer c -> w a.
data CodecFor r w c a = Codec { codecIn :: r a , codecOut :: c -> w a } type Codec r w a = CodecFor r w a a
With binary for example, r is Get and w is PutM. The reason we have an extra parameter c is so that we can associate a Codec with a particular field using the =. operator:
(=.) :: (c' -> c) -> CodecFor r w c a -> CodecFor r w c' a
Codec is an instance of Functor, Applicative, Monad and Profunctor. You can serialize in any order you like, regardless of field order in the data type:
recordBCodecFlipped :: BinaryCodec RecordB recordBCodecFlipped = do bs64 <- recordBByteString64 =. byteString 64 RecordB <$> recordBWord16 =. word16host <*> pure bs64
Contributors
=. operator and Profunctor approach thanks to Xia Li-yao