Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/etorreborre/registry-aeson
https://github.com/etorreborre/registry-aeson
Last synced: 7 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/etorreborre/registry-aeson
- Owner: etorreborre
- License: other
- Created: 2022-05-01T08:58:56.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2023-12-27T00:29:26.000Z (11 months ago)
- Last Synced: 2024-05-01T22:38:51.353Z (7 months ago)
- Language: Haskell
- Size: 213 KB
- Stars: 5
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE.txt
Awesome Lists containing this project
README
# `registry-aeson`
##### *It's functions all the way down*
### Presentation
This library is an add-on to [`registry`](https://github.com/etorreborre/registry), providing customizable encoders / decoders for [Aeson](https://hackage.haskell.org/package/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:
```haskell
{-# 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 Protoludenewtype 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 DateTimeencoders :: Registry _ _
encoders =
$(makeEncoder ''Delivery)
<: $(makeEncoder ''Person)
<: $(makeEncoder ''Email)
<: $(makeEncoder ''Identifier)
<: fun datetimeEncoder
<: jsonEncoder @Text
<: jsonEncoder @Int
<: defaultEncoderOptionsdatetimeEncoder :: 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:
```haskell
let encoderPerson = make @(Encoder Person) encoders
let encoded = encodeValue encoderPerson (Person (Identifier 123) (Email "[email protected]")) :: 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:
```haskell
{-# 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 Protoludenewtype 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 DateTimedecoders :: Registry _ _
decoders =
$(makeDecoder ''Delivery)
<: $(makeDecoder ''Person)
<: $(makeDecoder ''Email)
<: $(makeDecoder ''Identifier)
<: fun dateTimeDecoder
<: jsonDecoder @Text
<: jsonDecoder @Int
<: defaultDecoderOptionsdatetimeDecoder :: 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:
```haskell
let decoderPerson = make @(Decoder Person) decoders
let decoded = decode decoderPerson $ ObjectArray [Number 123, ObjectStr "[email protected]"]
```##### 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:
```haskell
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:
```haskell
-- 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:
```haskell
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.