Copyright | (c) Anton Gushcha, 2016 |
---|---|
License | MIT |
Maintainer | ncrashed@gmail.com |
Stability | experimental |
Portability | Portable |
Safe Haskell | None |
Language | Haskell2010 |
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 simpleToJSON
andFromJSON
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]
or simply WithField
"id" NewsId News
wraps you data type
and injects "id" field in produced JSON values:WithId
NewsId News
>>>
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" }
- data WithField s a b = WithField !a !b
- type WithId i a = WithField "id" i a
- data WithFields a b = WithFields !a !b
Single field injector
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 !a !b |
(Eq a, Eq b) => Eq (WithField s a b) Source # | |
(Read a, Read b) => Read (WithField s a b) Source # | |
(Show a, Show b) => Show (WithField s a b) Source # | |
Generic (WithField s a b) Source # | |
(KnownSymbol s, ToJSON a, ToJSON b) => ToJSON (WithField s a b) Source # | Note: the instance injects field only in Example of wrapper: { "id": 0, "value": [1, 2, 3] } |
(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 |
(NFData a, NFData b) => NFData (WithField s a b) Source # | |
(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 |
type Rep (WithField s a b) Source # | |
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 Object
subtype of aeson Value
. If it is not true, a wrapper
layer is introduced.
If a
is not a Object
, the wrapper contains injected
field with body of a
.
If b
is not a Object
, the wrapper contains value
field with body of b
.
If both are not Object
, the wrapper contains injected
and value
keys with
a
and b
respectively.
WithFields !a !b |
(Eq a, Eq b) => Eq (WithFields a b) Source # | |
(Read a, Read b) => Read (WithFields a b) Source # | |
(Show a, Show b) => Show (WithFields a b) Source # | |
Generic (WithFields a b) Source # | |
(ToJSON a, ToJSON b) => ToJSON (WithFields a b) Source # | Note: the instance injects field only in Example of wrapper when { "field1": 0, "field2": "val", "value": [1, 2, 3] } Example of wrapper when { "field1": 0, "field2": "val", "injected": [1, 2, 3] } Example of wrapper when as { "value": 42, "injected": [1, 2, 3] } |
(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 |
(NFData a, NFData b) => NFData (WithFields a b) Source # | |
(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 |
type Rep (WithFields a b) Source # | |