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
Downloads 135 total (67 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.