aeson-injector-1.1.3.0: Injecting fields into aeson values

Copyright(c) Anton Gushcha 2016
LicenseMIT
Maintainerncrashed@gmail.com
Stabilityexperimental
PortabilityPortable
Safe HaskellNone
LanguageHaskell2010

Data.Aeson.WithField

Contents

Description

When builds a RESTful API one often faces the problem that some methods need inbound data without unique identifier (for instance, a creation of new resource) and some methods need the same outbound data with additional fields attached to the response.

The module provides you with WithField and WithFields data types that help you to solve the issue without code duplication.

It is small utility library that is intented to be used in RESTful APIs, especially with servant and Swagger. Its main purpose is simple injection of fields into JSONs produced by aeson library.

Consider the following common data type in web service developing:

data News = News {
  title :: Text
, body :: Text
, author :: Text
, timestamp :: UTCTime
}

-- Consider we have simple ToJSON and FromJSON instances
$(deriveJSON defaultOptions ''News)

ToJSON instance produces JSON's like:

{
  "title": "Awesome piece of news!"
, "body": "Big chunk of text"
, "author": "Just Me"
, "timestamp": "2016-07-26T18:54:42.678999Z"
}

Now one can create a simple web server with servant DSL:

type NewsId = Word

type NewsAPI =
       ReqBody '[JSON] News :> Post '[JSON] NewsId
  :<|> Capture "news-id" NewsId :> Get '[JSON] News
  :<|> "list" :> Get '[JSON] [News]

All seems legit, but, wait a second, an API user definitely would like to know id of news in the "list" method. One way to do this is declare new data type NewsInfo with additional field, but it is bad solution as requires to code duplication for each resource.

So, here aeson-injector steps in, now you can write:

type NewsAPI =
       ReqBody '[JSON] News :> Post '[JSON] NewsId
  :<|> Capture "news-id" NewsId :> Get '[JSON] News
  :<|> "list" :> Get '[JSON] [WithField "id" NewsId News]

WithField "id" NewsId News or simply WithId NewsId News wraps you data type and injects "id" field in produced JSON values:

>>> encode (WithField 42 myNews :: WithField "id" NewsId News)
{
  "id": 42
, "title": "Awesome piece of news!"
, "body": "Big chunk of text"
, "author": "Just Me"
, "timestamp": "2016-07-26T18:54:42.678999Z"
}

WithField data type has FromJSON instance for seamless parsing of data with injected fields and ToSchema instance for servant-swagger support.

Injecting multiple values

The library also has more general data type 'WithFields a b' that injects fields of 'toJSON a' into 'toJSON b'.

 haskell
data NewsPatch = NewsPatch {
  taggs :: [Text]
, rating :: Double
}
$(deriveJSON defaultOptions ''NewsPatch)
 haskell
let myNewsPatch = NewsPatch ["tag1", "tag2"] 42
in encode $ WithFields myNewsPatch myNews
{
  "title": "Awesome piece of news!"
, "body": "Big chunk of text"
, "author": "Just Me"
, "timestamp": "2016-07-26T18:54:42.678999Z"
, "tags": ["tag1", "tag2"]
, "rating": 42.0
}

Corner cases

Unfortunately, we cannot inject in non object values of produced JSON, so the library creates a wrapper object around non-object value:

encode (WithId 0 "non-object" :: WithId Int String)
{
  "id": 0
, "value": "non-object"
}

The same story is about WithFields data type:

encode (WithFields 0 "non-object" :: WithFields Int String)
{
  "injected": 0
, "value": "non-object"
}
Synopsis

Single field injector

data WithField (s :: Symbol) a b Source #

Injects field a into b with tag s. It has special instances for ToJSON and FromJSON for such injection and corresponding Swagger ToSchema instance.

For instance:

>>> encode (WithField "val" (Left 42) :: WithField "injected" String (Either Int Int))
"{\"Left\":42,\"id\":\"val\"}"

If the instance cannot inject field (in case of single values and arrays), it wraps the result in the following way:

>>> encode (WithField "val" 42 :: WithField "injected" String Int)
"{\"value\":42,\"injected\":\"val\"}"

`WithField s a b` always overwites field s in JSON produced by b.

Constructors

WithField !a !b 
Instances
Bifunctor (WithField s) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

bimap :: (a -> b) -> (c -> d) -> WithField s a c -> WithField s b d #

first :: (a -> b) -> WithField s a c -> WithField s b c #

second :: (b -> c) -> WithField s a b -> WithField s a c #

Functor (WithField s a) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

fmap :: (a0 -> b) -> WithField s a a0 -> WithField s a b #

(<$) :: a0 -> WithField s a b -> WithField s a a0 #

(Eq a, Eq b) => Eq (WithField s a b) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

(==) :: WithField s a b -> WithField s a b -> Bool #

(/=) :: WithField s a b -> WithField s a b -> Bool #

(Read a, Read b) => Read (WithField s a b) Source # 
Instance details

Defined in Data.Aeson.WithField

(Show a, Show b) => Show (WithField s a b) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

showsPrec :: Int -> WithField s a b -> ShowS #

show :: WithField s a b -> String #

showList :: [WithField s a b] -> ShowS #

Generic (WithField s a b) Source # 
Instance details

Defined in Data.Aeson.WithField

Associated Types

type Rep (WithField s a b) :: Type -> Type #

Methods

from :: WithField s a b -> Rep (WithField s a b) x #

to :: Rep (WithField s a b) x -> WithField s a b #

(KnownSymbol s, ToJSON a, ToJSON b) => ToJSON (WithField s a b) Source #

Note: the instance injects field only in Value case. In other cases it forms a wrapper around the Value produced by toJSON of inner b body.

Example of wrapper:

{ "id": 0, "value": [1, 2, 3] }
Instance details

Defined in Data.Aeson.WithField

Methods

toJSON :: WithField s a b -> Value #

toEncoding :: WithField s a b -> Encoding #

toJSONList :: [WithField s a b] -> Value #

toEncodingList :: [WithField s a b] -> Encoding #

(KnownSymbol s, FromJSON a, FromJSON b) => FromJSON (WithField s a b) Source #

Note: the instance tries to parse the json as object with additional field value, if it fails it assumes that it is a wrapper produced by corresponding ToJSON instance.

Note: The instance tries to parse the b part without s field at first time. If it fails, the instance retries with presence of the s field.

Instance details

Defined in Data.Aeson.WithField

Methods

parseJSON :: Value -> Parser (WithField s a b) #

parseJSONList :: Value -> Parser [WithField s a b] #

(NFData a, NFData b) => NFData (WithField s a b) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

rnf :: WithField s a b -> () #

(ToSample a, ToSample b) => ToSample (WithField s a b) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

toSamples :: Proxy (WithField s a b) -> [(Text, WithField s a b)] #

(KnownSymbol s, ToSchema a, ToSchema b) => ToSchema (WithField s a b) Source #

Note: the instance tries to generate schema of the json as object with additional field value, if it fails it assumes that it is a wrapper produced by corresponding ToJSON instance.

Instance details

Defined in Data.Aeson.WithField

type Rep (WithField s a b) Source # 
Instance details

Defined in Data.Aeson.WithField

type Rep (WithField s a b) = D1 (MetaData "WithField" "Data.Aeson.WithField" "aeson-injector-1.1.3.0-6MY5WVAvsf2AfVianu6uUb" False) (C1 (MetaCons "WithField" PrefixI False) (S1 (MetaSel (Nothing :: Maybe Symbol) NoSourceUnpackedness SourceStrict DecidedStrict) (Rec0 a) :*: S1 (MetaSel (Nothing :: Maybe Symbol) NoSourceUnpackedness SourceStrict DecidedStrict) (Rec0 b)))

type WithId i a = WithField "id" i a Source #

Workaround for a problem that is discribed as: sometimes I need a id with the data, sometimes not.

The important note that ToJSON and FromJSON instances behaves as it is a but with additional id field.

Multiple fields injector

data WithFields a b Source #

Merge fields of a into b, more general version of WithField.

The usual mode of the data type assumes that ToJSON instances of a and b produce Value subtype of aeson Value. If it is not true, a wrapper layer is introduced.

If a is not a Value, the wrapper contains injected field with body of a. If b is not a Value, the wrapper contains value field with body of b. If both are not Value, the wrapper contains injected and value keys with a and b respectively.

Constructors

WithFields !a !b 
Instances
Bifunctor WithFields Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

bimap :: (a -> b) -> (c -> d) -> WithFields a c -> WithFields b d #

first :: (a -> b) -> WithFields a c -> WithFields b c #

second :: (b -> c) -> WithFields a b -> WithFields a c #

Functor (WithFields a) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

fmap :: (a0 -> b) -> WithFields a a0 -> WithFields a b #

(<$) :: a0 -> WithFields a b -> WithFields a a0 #

(Eq a, Eq b) => Eq (WithFields a b) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

(==) :: WithFields a b -> WithFields a b -> Bool #

(/=) :: WithFields a b -> WithFields a b -> Bool #

(Read a, Read b) => Read (WithFields a b) Source # 
Instance details

Defined in Data.Aeson.WithField

(Show a, Show b) => Show (WithFields a b) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

showsPrec :: Int -> WithFields a b -> ShowS #

show :: WithFields a b -> String #

showList :: [WithFields a b] -> ShowS #

Generic (WithFields a b) Source # 
Instance details

Defined in Data.Aeson.WithField

Associated Types

type Rep (WithFields a b) :: Type -> Type #

Methods

from :: WithFields a b -> Rep (WithFields a b) x #

to :: Rep (WithFields a b) x -> WithFields a b #

(ToJSON a, ToJSON b) => ToJSON (WithFields a b) Source #

Note: the instance injects field only in Value case. In other cases it forms a wrapper around the Value produced by toJSON of inner b body.

Example of wrapper when b is not a Value, b goes into "value" field:

{ "field1": 0, "field2": "val", "value": [1, 2, 3] }

Example of wrapper when a is not a Value, but b is. a goes into "injected" field:

{ "field1": 0, "field2": "val", "injected": [1, 2, 3] }

Example of wrapper when as a is not a Value, as b is not. a goes into "injected" field, b goes into "value" field:

{ "value": 42, "injected": [1, 2, 3] }

`WithFields a b` always overwites fields in JSON produced by b with fields from JSON produced by a.

Instance details

Defined in Data.Aeson.WithField

(ToJSON a, FromJSON a, FromJSON b) => FromJSON (WithFields a b) Source #

Note: the instance tries to parse the json as object with additional field value, if it fails it assumes that it is a wrapper produced by corresponding ToJSON instance.

Note: The instance tries to parse the b part without fields of a at first time. If it fails, the instance retries with presence of a's fields.

The implementation requires `ToJSON a` to catch fields of a and it is assumed that `fromJSON . toJSON === id` for a.

Instance details

Defined in Data.Aeson.WithField

(NFData a, NFData b) => NFData (WithFields a b) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

rnf :: WithFields a b -> () #

(ToSample a, ToSample b) => ToSample (WithFields a b) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

toSamples :: Proxy (WithFields a b) -> [(Text, WithFields a b)] #

(ToSchema a, ToSchema b) => ToSchema (WithFields a b) Source #

Note: the instance tries to generate schema of the json as object with additional field value, if it fails it assumes that it is a wrapper produced by corresponding ToJSON instance.

Instance details

Defined in Data.Aeson.WithField

type Rep (WithFields a b) Source # 
Instance details

Defined in Data.Aeson.WithField

type Rep (WithFields a b) = D1 (MetaData "WithFields" "Data.Aeson.WithField" "aeson-injector-1.1.3.0-6MY5WVAvsf2AfVianu6uUb" False) (C1 (MetaCons "WithFields" PrefixI False) (S1 (MetaSel (Nothing :: Maybe Symbol) NoSourceUnpackedness SourceStrict DecidedStrict) (Rec0 a) :*: S1 (MetaSel (Nothing :: Maybe Symbol) NoSourceUnpackedness SourceStrict DecidedStrict) (Rec0 b)))

Single field wrapper

newtype OnlyField (s :: Symbol) a Source #

Special case, when you want to wrap your type a in field with name s.

>>> encode (OnlyField 0 :: OnlyField "id" Int)
"{\"id\":0}"
>>> encode $ toSchema (Proxy :: Proxy (OnlyField "id" Int))
"{\"required\":[\"id\"],\"type\":\"object\",\"properties\":{\"id\":{\"maximum\":9223372036854775807,\"minimum\":-9223372036854775808,\"type\":\"integer\"}}}"

Also the type can be used as an endpoint for WithField:

>>> encode (WithField True (OnlyField 0) :: WithField "val" Bool (OnlyField "id" Int))
"{\"id\":0,\"val\":true}"

Constructors

OnlyField 

Fields

Instances
Functor (OnlyField s) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

fmap :: (a -> b) -> OnlyField s a -> OnlyField s b #

(<$) :: a -> OnlyField s b -> OnlyField s a #

Eq a => Eq (OnlyField s a) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

(==) :: OnlyField s a -> OnlyField s a -> Bool #

(/=) :: OnlyField s a -> OnlyField s a -> Bool #

Read a => Read (OnlyField s a) Source # 
Instance details

Defined in Data.Aeson.WithField

Show a => Show (OnlyField s a) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

showsPrec :: Int -> OnlyField s a -> ShowS #

show :: OnlyField s a -> String #

showList :: [OnlyField s a] -> ShowS #

Generic (OnlyField s a) Source # 
Instance details

Defined in Data.Aeson.WithField

Associated Types

type Rep (OnlyField s a) :: Type -> Type #

Methods

from :: OnlyField s a -> Rep (OnlyField s a) x #

to :: Rep (OnlyField s a) x -> OnlyField s a #

(KnownSymbol s, ToJSON a) => ToJSON (OnlyField s a) Source # 
Instance details

Defined in Data.Aeson.WithField

(KnownSymbol s, FromJSON a) => FromJSON (OnlyField s a) Source # 
Instance details

Defined in Data.Aeson.WithField

ToSample a => ToSample (OnlyField s a) Source # 
Instance details

Defined in Data.Aeson.WithField

Methods

toSamples :: Proxy (OnlyField s a) -> [(Text, OnlyField s a)] #

(KnownSymbol s, ToSchema a) => ToSchema (OnlyField s a) Source # 
Instance details

Defined in Data.Aeson.WithField

type Rep (OnlyField s a) Source # 
Instance details

Defined in Data.Aeson.WithField

type Rep (OnlyField s a) = D1 (MetaData "OnlyField" "Data.Aeson.WithField" "aeson-injector-1.1.3.0-6MY5WVAvsf2AfVianu6uUb" True) (C1 (MetaCons "OnlyField" PrefixI True) (S1 (MetaSel (Just "unOnlyField") NoSourceUnpackedness NoSourceStrictness DecidedLazy) (Rec0 a)))

type OnlyId i = OnlyField "id" i Source #

Special case for the most common "id" field