waargonaut-0.8.0.1: JSON wrangling

Safe HaskellNone
LanguageHaskell2010

Waargonaut.Generic

Contents

Description

This module contains the types and functions that power the Generic functions for Waargonaut. Code that writes the code so you don't have to.

Synopsis

Rationale

Although creating your Decoders and Encoders explicitly is the preferred way of utilising Waargonaut. The Generic mechanism within Haskell provides immense opportunity to reduce or eliminate the need to write code. Given the mechanical nature of JSON this a benefit that cannot be ignored.

There are two typeclasses provided, JsonEncode and JsonDecode. Each with a single function that will generate a Encoder or Decoder for that type. Normally, typeclasses such as these are only parameterised over the type that is to be encoded/decoded. Which is acceptable if there is only ever a single possible way to encode or decode a value of that type. However this is rarely the case, even with respect to strings or numbers.

To account for this, the JsonEncode and JsonDecode typeclasses require an additional type parameter t . This parameter allows you to differentiate between the alternative ways of encoding or decoding a single type a . This parameter is attached to the Encoder or Decoder using the Tagged newtype. Allowing the type system to help you keep track of them.

Quick Start

A quick example on how to use the Waargonaut Generic functionality. We will use the following type and let GHC and Generic write our Encoder and Decoder for us.

data Image = Image
  { _imageWidth    :: Int
  , _imageHeight   :: Int
  , _imageTitle    :: Text
  , _imageAnimated :: Bool
  , _imageIDs      :: [Int]
  }
  deriving (Eq, Show)

Ensure we have the required imports and language options:

{-# LANGUAGE DeriveGeneric #-}
import qualified GHC.Generic as GHC
import Waargonaut.Generic (Generic, HasDatatypeInfo, JsonEncode, JsonDecode, GWaarg)

Update our data type 'deriving' to have GHC to do the heavy lifting:

data Image = Image
  ...
  deriving (..., GHC.Generic)

Because Waargonaut uses the 'generics-sop' package to make the Generic functions easier to write and maintain. We need two more instances, note that we don't have to write these either. We can leave these empty and the default implementations, courtesy of Generic, will handle it for us.

instance HasDatatypeInfo Image
instance Generic Image

Now we can define our JsonEncode and JsonDecode instances. We need to provide the t parameter. Assume we have no special requirements, so we can use the GWaarg tag.

instance JsonEncode GWaarg Image
instance JsonDecode GWaarg Image

That's it! We can now use mkEncoder and mkDecoder to write the code for our Image type. These will be tagged with our GWaarg phantom type parameter:

mkEncoder :: Applicative f => Tagged GWaarg (Encoder f Image)
mkDecoder :: Monad f       => Tagged GWaarg (Decoder f Image)

The encoding and decoding "runner" functions will require that you remove the tag. You can use the untag function for this. The next section will discuss the Tagged type.

There is Template Haskell available that can write all of the Generic deriving for you, see the 'Generics.SOP.TH' module in the 'generics-sop' package for more. Given how little boilerplate code is required and that the Template Haskell extension enforces a strict ordering of code within the file. It is not the recommended solution. But I'm not your supervisor, I'm just a library.

Tagged

The Tagged type comes from the 'tagged' package. It is a 'newtype' that provides a phantom type parameter. As well as having a several useful typeclass instances and helpful functions already written for us.

When dealing with the Tagged Encoders and Decoders there are two functions that are particularly useful; untag, and proxy.

The untag function removes the tag from the inner type:

untag :: -- forall k (s :: k) b. Tagged s b -> b

When used with one of the Tagged Generic functions:

let e = mkEncoder :: Applicative f => Tagged GWaarg (Encoder f Image)

untag e :: Applicative f => Encoder f Image

The other function proxy, allows you to use mkEncoder or mkDecoder with the desired t parameter and then immediately remove the tag. This function requires the use of some proxy that carries the same t of your instance:

proxy :: Tagged s a -> proxy s -> a

One way to utilise this function is in combination with Proxy from base:

(proxy mkDecoder (Proxy :: Proxy GWaarg)) :: Monad f => Decoder f Image

This lets you skip the untag step but without losing the safety of the Tagged phantom type.

GHC >= 8 Convenience

All of the techniques described above are explicit and will work in all versions of GHC that Waargonaut supports. Should you be running a GHC that is version 8.0.1 or later, then you have access to a language extension called TypeApplications.

This extension allows you to avoid much of the explicit type annotations described in Tagged section of Waargonaut.Generic. For example the proxy function may be utilised like so:

(proxy mkDecoder (Proxy :: Proxy GWaarg)) :: Monad f => Decoder f Image

Becomes:

(proxy mkDecoder @GWaarg) :: Monad f => Decoder f Image

You can also use the TypeApplications directly on the mkEncoder or mkDecoder function:

mkEncoder @GWaarg :: Applicative f => Tagged GWaarg (Encoder f Image)
mkDecoder @GWaarg :: Monad f       => Tagged GWaarg (Decoder f Image)

TypeClasses

class JsonEncode t a where Source #

Encoding Typeclass for Waargonaut.

This type class is responsible for creating an Encoder for the type of a , differentiated from the other possible instances of this typeclass for type a by the tag type t .

To create a Tagged Encoder for the purposes of writing an instance your self, you need only data constructor Tagged from Tagged. It has been re-exported from this module.

instance JsonEncode GWaarg Foo where
  mkEncoder = Tagged fooEncoderIWroteEarlier

Minimal complete definition

Nothing

Instances
JsonEncode (t :: k) Json Source # 
Instance details

Defined in Waargonaut.Generic

JsonEncode (t :: k) Bool Source # 
Instance details

Defined in Waargonaut.Generic

JsonEncode (t :: k) Scientific Source # 
Instance details

Defined in Waargonaut.Generic

JsonEncode (t :: k) Int Source # 
Instance details

Defined in Waargonaut.Generic

JsonEncode (t :: k) Text Source # 
Instance details

Defined in Waargonaut.Generic

JsonEncode t a => JsonEncode (t :: k) (NonEmpty a) Source # 
Instance details

Defined in Waargonaut.Generic

JsonEncode t a => JsonEncode (t :: k) [a] Source # 
Instance details

Defined in Waargonaut.Generic

Methods

mkEncoder :: Applicative f => Tagged t (Encoder f [a]) Source #

JsonEncode t a => JsonEncode (t :: k) (Maybe a) Source # 
Instance details

Defined in Waargonaut.Generic

Methods

mkEncoder :: Applicative f => Tagged t (Encoder f (Maybe a)) Source #

(JsonEncode t a, JsonEncode t b) => JsonEncode (t :: k) (Either a b) Source # 
Instance details

Defined in Waargonaut.Generic

Methods

mkEncoder :: Applicative f => Tagged t (Encoder f (Either a b)) Source #

class JsonDecode t a where Source #

Decoding Typeclass for Waargonaut

Responsible for creating a Decoder for the type a , differentiated from the other possible instances of this typeclass for type a by the tag type t .

To create a Tagged Decoder for the purposes of writing an instance your self, you need only data constructor Tagged from Tagged. It has been re-exported from this module.

instance JsonDecode GWaarg Foo where
  mkDecoder = Tagged fooDecoderIWroteEarlier

Minimal complete definition

Nothing

Instances
JsonDecode (t :: k) Json Source # 
Instance details

Defined in Waargonaut.Generic

Methods

mkDecoder :: Monad f => Tagged t (Decoder f Json) Source #

JsonDecode (t :: k) Bool Source # 
Instance details

Defined in Waargonaut.Generic

Methods

mkDecoder :: Monad f => Tagged t (Decoder f Bool) Source #

JsonDecode (t :: k) Scientific Source # 
Instance details

Defined in Waargonaut.Generic

JsonDecode (t :: k) Int Source # 
Instance details

Defined in Waargonaut.Generic

Methods

mkDecoder :: Monad f => Tagged t (Decoder f Int) Source #

JsonDecode (t :: k) Text Source # 
Instance details

Defined in Waargonaut.Generic

Methods

mkDecoder :: Monad f => Tagged t (Decoder f Text) Source #

JsonDecode t a => JsonDecode (t :: k) (NonEmpty a) Source # 
Instance details

Defined in Waargonaut.Generic

Methods

mkDecoder :: Monad f => Tagged t (Decoder f (NonEmpty a)) Source #

JsonDecode t a => JsonDecode (t :: k) [a] Source # 
Instance details

Defined in Waargonaut.Generic

Methods

mkDecoder :: Monad f => Tagged t (Decoder f [a]) Source #

JsonDecode t a => JsonDecode (t :: k) (Maybe a) Source # 
Instance details

Defined in Waargonaut.Generic

Methods

mkDecoder :: Monad f => Tagged t (Decoder f (Maybe a)) Source #

(JsonDecode t a, JsonDecode t b) => JsonDecode (t :: k) (Either a b) Source # 
Instance details

Defined in Waargonaut.Generic

Methods

mkDecoder :: Monad f => Tagged t (Decoder f (Either a b)) Source #

Tag

data GWaarg Source #

This is a provided tag that may be used for tagging the JsonEncode and JsonDecode instances. You are encouraged to make your own tags for full control of your own instances.

Options

data NewtypeName Source #

The options we currently have for using the Generic mechanism to handle 'newtype' values:

Constructors

Unwrap

Discard the newtype wrapper and encode the inner value.

newtype Foo = Foo Text

let x = Foo "Fred"

Will be encoded as: "Fred"

ConstructorNameAsKey

Encode the newtype value as an object using the constructor as the "key".

newtype Foo = Foo Text

let x = Foo "Fred"

Will be encoded as: {"Foo":"Fred"}

FieldNameAsKey

Encode the newtype value as an object, treaing the field accessor as the "key", and passing that field name through the _optionsFieldName function.

newtype Foo = Foo { deFoo :: Text }

let x = Foo "Fred"

Will be encoded as: {"deFoo":"Fred"}

Instances
Eq NewtypeName Source # 
Instance details

Defined in Waargonaut.Generic

Show NewtypeName Source # 
Instance details

Defined in Waargonaut.Generic

data Options Source #

The configuration options for creating Generic encoder or decoder values.

Constructors

Options 

Fields

defaultOpts :: Options Source #

Default options for Generic functionality:

  • Field names are left untouched: (id)
  • Newtype values are encoded as raw values: (Unwrap)

trimPrefixLowerFirst :: Text -> String -> String Source #

Helper function to alter record field names for encoding and decoding. Intended use is to be given the prefix you would like to have removed and then included in the Options for the typeclass you are implementing.

A common use case when encoding Haskell record types is to remove a prefix and then lower-case the first letter:

>>> trimPrefixLowerFirst "_image" "_imageHeight"
"height"
>>> trimPrefixLowerFirst "_image" "Height"
"Height"
>>> trimPrefixLowerFirst "_image" ""
""
>>> trimPrefixLowerFirst "" "_imageHeight"
"_imageHeight"

Creation

gEncoder :: forall t a f. (Generic a, Applicative f, HasDatatypeInfo a, All2 (JsonEncode t) (Code a)) => Options -> Tagged t (Encoder f a) Source #

Create a Tagged Encoder for type a , tagged by t , using the given Options.

Combined with the defaultOpts this is the default implementation of JsonEncode.

Some examples:

instance JsonEncode GWaarg Image where
  mkEncoder = gEncoder defaultOpts
instance JsonEncode GWaarg Image where
  mkEncoder = gEncoder (defaultOpts { _optionsFieldName = trimPrefixLowerFirst "_image" })

gDecoder :: forall f a t. (Generic a, HasDatatypeInfo a, All2 (JsonDecode t) (Code a), Monad f) => Options -> Tagged t (Decoder f a) Source #

Create a Tagged Decoder for type a , tagged by t , using the given Options.

Combined with the defaultOpts this is the default implementation of JsonEncode.

Some examples:

instance JsonEncode GWaarg Image where
  mkDecoder = gDecoder defaultOpts
instance JsonEncode GWaarg Image where
  mkDecoder = gDecoder (defaultOpts { _optionsFieldName = trimPrefixLowerFirst "_image" })

gObjEncoder :: forall t a f xs. (Generic a, Applicative f, HasDatatypeInfo a, All2 (JsonEncode t) (Code a), IsRecord a xs) => Options -> Tagged t (ObjEncoder f a) Source #

Create a Tagged ObjEncoder for type a , tagged by t .

This isn't compatible with the JsonEncode typeclass because it creates an ObjEncoder and for consistency reasons the JsonEncode typeclass produces Encoders.

However it lets you more easily access the Contravariant functionality that is part of the ObjEncoder type.

data Foo = Foo { fooA :: Text, fooB :: Int } deriving (Eq, Show)
deriveGeneric ''Foo

objEncFoo :: Applicative f => ObjEncoder f Foo
objEncFoo = untag $ gObjEncoder (defaultOps { _optionsFieldName = drop 3 })

NB: This function overrides the newtype options to use the FieldNameAsKey option to be consistent with the behaviour of the record encoding.

Reexports

class All (SListI :: [Type] -> Constraint) (Code a) => Generic a where #

The class of representable datatypes.

The SOP approach to generic programming is based on viewing datatypes as a representation (Rep) built from the sum of products of its components. The components of a datatype are specified using the Code type family.

The isomorphism between the original Haskell datatype and its representation is witnessed by the methods of this class, from and to. So for instances of this class, the following laws should (in general) hold:

to . from === id :: a -> a
from . to === id :: Rep a -> Rep a

You typically don't define instances of this class by hand, but rather derive the class instance automatically.

Option 1: Derive via the built-in GHC-generics. For this, you need to use the DeriveGeneric extension to first derive an instance of the Generic class from module GHC.Generics. With this, you can then give an empty instance for Generic, and the default definitions will just work. The pattern looks as follows:

import qualified GHC.Generics as GHC
import Generics.SOP

...

data T = ... deriving (GHC.Generic, ...)

instance Generic T -- empty
instance HasDatatypeInfo T -- empty, if you want/need metadata

Option 2: Derive via Template Haskell. For this, you need to enable the TemplateHaskell extension. You can then use deriveGeneric from module Generics.SOP.TH to have the instance generated for you. The pattern looks as follows:

import Generics.SOP
import Generics.SOP.TH

...

data T = ...

deriveGeneric ''T -- derives HasDatatypeInfo as well

Tradeoffs: Whether to use Option 1 or 2 is mainly a matter of personal taste. The version based on Template Haskell probably has less run-time overhead.

Non-standard instances: It is possible to give Generic instances manually that deviate from the standard scheme, as long as at least

to . from === id :: a -> a

still holds.

Minimal complete definition

Nothing

Associated Types

type Code a :: [[Type]] #

The code of a datatype.

This is a list of lists of its components. The outer list contains one element per constructor. The inner list contains one element per constructor argument (field).

Example: The datatype

data Tree = Leaf Int | Node Tree Tree

is supposed to have the following code:

type instance Code (Tree a) =
  '[ '[ Int ]
   , '[ Tree, Tree ]
   ]

Methods

from :: a -> Rep a #

Converts from a value to its structural representation.

to :: Rep a -> a #

Converts from a structural representation back to the original value.

class Generic a => HasDatatypeInfo a where #

A class of datatypes that have associated metadata.

It is possible to use the sum-of-products approach to generic programming without metadata. If you need metadata in a function, an additional constraint on this class is in order.

You typically don't define instances of this class by hand, but rather derive the class instance automatically. See the documentation of Generic for the options.

Minimal complete definition

Nothing

Associated Types

type DatatypeInfoOf a :: DatatypeInfo #

Type-level datatype info

Methods

datatypeInfo :: proxy a -> DatatypeInfo (Code a) #

Term-level datatype info; by default, the term-level datatype info is produced from the type-level info.