@hackage registry-messagepack0.3.1.0

MessagePack encoders / decoders

registry-messagepack

It's functions all the way down

Presentation

This library is an add-on to registry, providing customizable encoders / decoders for MessagePack. The approach taken is to add to a registry a list of functions taking encoders / decoders as parameters and producing encoders / decoders. Then registry is able to assemble all the functions required to make an Encoder or a Decoder of a given type if the encoders or decoders for its dependencies can be made out of the registry.

Encoders

Example

Here is an example of creating encoders for a set of related data types:

{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}

import Data.MessagePack
import Data.Registry
import Data.Registry.MessagePack.Encoder
import Data.Time
import Data.Vector
import Protolude

newtype Identifier = Identifier Int
newtype Email = Email { _email :: Text }
newtype DateTime = DateTime { _datetime :: UTCTime }
data Person = Person { identifier :: Identifier, email :: Email }

data Delivery =
    NoDelivery
  | ByEmail Email
  | InPerson Person DateTime

encoders :: Registry _ _
encoders =
  $(makeEncoder ''Delivery)
  <: $(makeEncoder ''Person)
  <: $(makeEncoder ''Email)
  <: $(makeEncoder ''Identifier)
  <: fun datetimeEncoder
  <: messagePackEncoder @Text
  <: messagePackEncoder @Int

datetimeEncoder :: Encoder DateTime
datetimeEncoder = Encoder (ObjectStr . formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S%3QZ" . _datetime)

In the code above most encoders are created with TemplateHaskell and the makeEncoder function. The other encoders are either:

  • created manually: dateTimeEncoder (note that this encoder needs to be added to the registry with fun)
  • retrieved from a MessagePack instance: messagePackEncoder @Text, messagePackEncoder @Int

Given the list of encoders an Encoder Person can be retrieved with:

let encoderPerson = make @(Encoder Person) encoders
let encoded = encode encoderPerson (Person (Identifier 123) (Email "me@here.com")) :: Object

Generated encoders

The makeEncoder function makes the following functions:

-- makeEncoder ''Identifier
\(e::Encoder Int) -> Encoder $ \(Identifier n) -> encode e n

-- makeEncoder ''Email
\(e::Encoder Text) -> Encoder $ \(Email s) -> encode e s

-- makeEncoder ''Person
\(e1::Encoder Identifier) (e2::Encoder Email) -> Encoder $ \(Person a1 a2) -> ObjectArray [encode e1 a1, encode e2 a2]

-- makeEncoder ''Delivery
\(e1::Encoder Email) (e2::Encoder Person) (e3::Encoder DateTime) -> Encoder $ \case
  NoDelivery -> ObjectArray [ObjectInt 0]
  ByEmail a1 -> ObjectArray [ObjectInt 1, encode e1 a1]
  InPerson a1 a2 -> ObjectArray [ObjectInt 2, encode e1 a1, encode e2 a2]

NOTE this function does not support recursive data types (and much less mutually recursive data types)

Decoders

Example

Here is an example of creating decoders for a set of related data types:

{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}

import Data.MessagePack
import Data.Registry
import Data.Registry.MessagePack.Decoder
import Data.Time
import Protolude

newtype Identifier = Identifier Int
newtype Email = Email { _email :: Text }
newtype DateTime = DateTime { _datetime :: UTCTime }
data Person = Person { identifier :: Identifier, email :: Email }

data Delivery =
    NoDelivery
  | ByEmail Email
  | InPerson Person DateTime

decoders :: Registry _ _
decoders =
  $(makeDecoder ''Delivery)
  <: $(makeDecoder ''Person)
  <: $(makeDecoder ''Email)
  <: $(makeDecoder ''Identifier)
  <: fun dateTimeDecoder
  <: messagePackDecoder @Text
  <: messagePackDecoder @Int

dateTimeDecoder :: Decoder DateTime
dateTimeDecoder = Decoder $ \case
  ObjectStr s -> DateTime <$> parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%QZ" s
  other -> Error ("not a valid DateTime: " <> show other)

In the code above most decoders are created with TemplateHaskell and the makeDecoder function. The other decoders are either:

  • created manually: dateTimeDecoder (note that this decoder needs to be added to the registry with fun)
  • retrieved from a MessagePack instance: messagePackDecoder @Text, messagePackDecoder @Int

Given the list of Decoders an Decoder Person can be retrieved with:

let decoderPerson = make @(Decoder Person) decoders
let decoded = decode decoderPerson $ ObjectArray [ObjectInt 123, ObjectStr "me@here.com"]

Generated decoders

The makeDecoder function makes the following functions:

-- makeDecoder ''Identifier
\(d::Decoder Int) -> Decoder $ \o -> Identifier <$> decode d o

-- makeDecoder ''Email
\(d::Decoder Text) -> Decoder $ \o -> Email <$> decode d o

-- makeDecoder ''Person
\(d1::Decoder Identifier) (d2::Decoder Email) -> Decoder $ \case
  ObjectArray [o1, o2] -> Person <$> decode d1 o1 <*> decode d2 o2
  other -> Error ("not a valid Person: " <> show other)

-- makeDecoder ''Delivery
\(d1::Decoder Email) (d2::Decoder Person) (d3::Decoder DateTime) -> Decoder $ \case
  ObjectArray [ObjectInt 0] -> pure NoDelivery
  ObjectArray [ObjectInt 1, o1] -> ByEmail <$> decode d1 o1
  ObjectArray [ObjectInt 2, o1, o2] -> InPerson <$> decode d1 o1 <*> decode d2 o2
  other -> Error ("not a valid Delivery: " <> show other)

NOTE this function does not support recursive data types (and much less mutually recursive data types)

Generation options

When using TemplateHaskell to generate encoders and decoders, all type names will be generated as unqualified by default. You can instead opt for qualified names by specifying an Options value:

import Data.Registry.MessagePack.Encoder
import Data.Registry.MessagePack.Options

-- DataTypes defines the Email and Person data types
import qualified DataTypes as DT
import qualified Data.Common

encoders =
  <: $(makeEncoderWith defaultOptions {modifyTypeName = qualified} ''DT.Person)
  <: $(makeEncoderWith defaultOptions {modifyTypeName = qualifiedAs "DT"} ''DT.Email)
  <: $(makeEncoderWith defaultOptions {modifyTypeName = qualifiedWithLastName} ''Common.Age)

NOTE only the data type and its constructors are qualified when you use qualifiedXXX functions