servant-hateoas: HATEOAS extension for servant

[ bsd3, hateoas, library, rest, servant, web ] [ Propose Tags ] [ Report a vulnerability ]

Create Resource-Representations for your types and make your API HATEOAS-compliant. Automatically derive a HATEOAS-API and server-implementation from your API or straight up define a HATEOAS-API yourself. Currently HAL+JSON is the only supported Content-Type. Work for further is on progress. For now only basic hypermedia-link derivations such as the self-link are automatically generated. Expect more sophisticated link-derivation e.g. for paging in the future. This library is highly experimental and subject to change.


[Skip to Readme]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0, 0.1.1, 0.2.0, 0.2.1, 0.2.2, 0.3.0, 0.3.1, 0.3.2, 0.3.3, 0.3.4
Change log CHANGELOG.md
Dependencies aeson (>=2.2.3 && <2.3), base (>=4.17.2 && <5), constrained-some (>=0.1.0 && <0.2), http-media (>=0.8.1 && <0.9), http-types (>=0.12.2 && <0.13), network-uri (>=2.6.1.0 && <2.7), servant (>=0.20.2 && <0.21), servant-server (>=0.20.2 && <0.21), singleton-bool (>=0.1.4 && <0.2), text (>=1.2.3.0 && <2.2) [details]
Tested with ghc ==9.4.8, ghc ==9.6.4, ghc ==9.8.2, ghc ==9.10.1
License BSD-3-Clause
Copyright © 2024 Julian Bruder
Author Julian Bruder
Maintainer julian.bruder@outlook.com
Category Servant, Web, REST, HATEOAS
Home page https://github.com/bruderj15/servant-hateoas
Bug tracker https://github.com/bruderj15/servant-hateoas/issues
Uploaded by bruderj15 at 2024-12-30T13:57:46Z
Distributions NixOS:0.3.4
Downloads 271 total (21 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for servant-hateoas-0.3.4

[back to package description]

Hackage Static Badge Haskell-CI

servant-hateoas

HATEOAS support for servant.

Infant state, highly experimental.

Find a motivating example further down this README.

What can we do already?

  • Derive a layered HATEOAS-API and a server-implementation from an API, basically what has been touched on here.
  • Derive a HATEOAS-API from an API by rewriting the API and its server-implementation
    • Wrapping the response types of your API with Resource-Representations
    • Automatically adding the self-link to every resource
    • Adding custom links to resources via instances for type-class ToResource
  • Directly write a HATEOAS-API yourself

What can we do better?

Deriving the layered HATEOAS-API from your API does not require your API to be structured in a certain way.

However, for rewriting your API we need you to specify your server-implementation as an instance of class HasHandler (bad name, should be HasServer - exists already).

This currently makes it tricky for APIs which have shared path segments, e.g. "api" :> (UserApi :<|> AddressApi)

Therefore we currently need an instance on each flattened endpoint of the API, e.g. for "api :> UserApi" and "api :> AddressApi".

What's on the horizon?

A lot. There are plenty of opportunities.

  • Merging the derived HATEOAS Layer-API with the rewritten HATEOAS API.
  • Automatically adding links for servant-pagination
  • Adding rich descriptions for Hypermedia-relations for content-types such as application/prs.hal-forms+json
  • ...

Media-Types

  • application/hal+json
  • application/vnd.collection+json: Work in progrress
  • application/prs.hal-forms+json: Soon
  • Others: Maybe

Client usage with MimeUnrender is not yet supported.

Example

Suppose we have users and addresses, where each user has an address:

data User = User { usrId :: Int, addressId :: Int, income :: Double } deriving stock (Generic, Show, Eq, Ord) deriving anyclass (ToJSON) data Address = Address { addrId :: Int, street :: String, city :: String } deriving stock (Generic, Show, Eq, Ord) deriving anyclass (ToJSON) 

We need to define how their resource-representation looks like:

-- default just wrapps an address to a resource instance ToResource res Address -- add a link to the address-resource with the relation "address" for the user-resource instance Resource res => ToResource res User where toResource _ ct usr = addRel ("address", mkAddrLink $ addressId usr) $ wrap usr where mkAddrLink = toRelationLink $ resourcifyProxy (Proxy @AddressGetOne) ct 

Further we define our API as usual:

type Api = UserApi :<|> AddressApi type UserApi = UserGetOne :<|> UserGetAll :<|> UserGetQuery type UserGetOne = "api" :> "user" :> Title "The user with the given id" :> Capture "id" Int :> Get '[JSON] User type UserGetAll = "api" :> "user" :> Get '[JSON] [User] type UserGetQuery = "api" :> "user" :> "query" :> QueryParam "addrId" Int :> QueryParam "income" Double :> Get '[JSON] User type AddressApi = AddressGetOne type AddressGetOne = "api" :> "address" :> Capture "id" Int :> Get '[JSON] Address 

Getting all the layers of the API in a HATEOAS way now is as simple as:

layerServer :: Server (Resourcify (MkLayers Api) (HAL JSON)) layerServer = getResourceServer (Proxy @Handler) (Proxy @(HAL JSON)) (Proxy @(MkLayers Api)) 

If we further want to rewrite our API to a HATEOAS-API, we need to define the server-implementation as an instance of HasHandler.

This is nothing but the usual servant-server implementation, just that the implementation is not floating around in the source code and instead is bound to a class instance.

instance HasHandler UserGetOne where getHandler _ _ = \uId -> return $ User uId 0 0 instance HasHandler UserGetAll where getHandler _ _ = return [User 1 1 1000, User 2 2 2000, User 42 3 3000] instance HasHandler UserGetQuery where getHandler _ _ = \mAddrId mIncome -> return $ User 42 (maybe 0 id mAddrId) (maybe 0 id mIncome) instance HasHandler AddressGetOne where getHandler _ _ = \aId -> return $ Address aId "Foo St" "BarBaz" 

Getting the rewritten HATEOAS-API and it's server-implementation is as simple as:

apiServer :: Server (Resourcify Api (HAL JSON)) apiServer = getResourceServer (Proxy @Handler) (Proxy @(HAL JSON)) (Proxy @Api) 

For now apiServer and layerServer exist in isolation, but the goal is to merge them into one.

When we now run the layerServer and request GET http://host:port/api/user/query, we get:

{ "_embedded": {}, "_links": { "addrId": { "href": "/api/user/query{?addrId}", "templated": true, "type": "application/hal+json" }, "income": { "href": "/api/user/query{?income}", "templated": true, "type": "application/hal+json" }, "self": { "href": "/api/user/query", "type": "application/hal+json" } } } 

Similar for userServer and GET http://host:port/api/user/42:

{ "_embedded": {}, "_links": { "address": { "href": "/api/address/0", "type": "application/hal+json" }, "self": { "href": "/api/user/42", "title": "The user with the given id", "type": "application/hal+json" } }, "addressId": 0, "income": 0, "usrId": 42 } 

The complete example can be found here.

Contact information

Contributions, critics and bug reports are welcome!

Please feel free to contact me through GitHub.