Skip to content

etorreborre/registry-aeson

Repository files navigation

registry-aeson

It's functions all the way down

Presentation

This library is an add-on to registry, providing customizable encoders / decoders for Aeson.

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.

By doing so we get all the advantages from using registry:

  • we can override the aeson Options for either a whole graph of data types or just one data type
  • we can easily provide a different encodings / decodings for one data type in a specific context (a Date can be formatted differently if it is a birth date or an acquisition date for example)
  • we can define incremental evolutions of an API, all mapping to the same underlying data model

Encoders

Example

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

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

import Data.Aeson
import Data.Registry
import Data.Registry.Aeson.Encoder
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

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

datetimeEncoder :: Encoder DateTime
datetimeEncoder = fromValue $ \(DateTime dt) -> do
  let formatted = toS $ formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" dt
  Object [("_datetime", String formatted)]

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 Aeson instance: jsonEncoder @Text, jsonEncoder @Int

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

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

Generated encoders

The makeEncoder function uses the defaultOptions added to the registry to produce the same values that a Generic ToJSON instance would produce.

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 DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}

import Data.Aeson
import Data.Registry
import Data.Registry.Aeson.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
  <: jsonDecoder @Text
  <: jsonDecoder @Int
  <: defaultDecoderOptions

datetimeDecoder :: Decoder DateTime
datetimeDecoder = Decoder $ \case
  String s ->
    case parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%QZ" $ toS s of
      Just t -> pure (DateTime t)
      Nothing -> Left ("cannot read a DateTime: " <> s)
  other -> Left $ "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 Aeson instance: jsonDecoder @Text, jsonDecoder @Int

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

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

There is a bit of flexibility in the way encoders are created with TemplateHaskell.

A custom ConstructorsEncoder can be added to the registry to tweak the generation:

newtype ConstructorEncoder = ConstructorEncoder
  { encodeConstructor :: Options -> FromConstructor -> (Value, Encoding)
  }

A ConstructorEncoder uses configuration options and type information extracted from given data type (with TemplateHaskell) in order to produce a Value and an Encoding.

If necessary you can provide your own options and reuse the default function to produce different encoders.

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 [Number 0] -> pure NoDelivery
  ObjectArray [Number 1, o1] -> ByEmail <$> decode d1 o1
  ObjectArray [Number 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)

Overriding the generated decoders

There is a bit of flexibility in the way decoders are created with TemplateHaskell.

A custom ConstructorsDecoder can be added to the registry to tweak the generation:

newtype ConstructorsDecoder = ConstructorsDecoder
  { decodeConstructors :: Options -> [ConstructorDef] -> Value -> Either Text [ToConstructor]
  }

This function extracts values for a set of constructor definitions and returns ToConstructor values containing a JSON Value to be decoded for each field of a given constructor (along with its name).

If necessary you can provide your own options and reuse the default function to produce different decoders.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published