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), 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-24T12:54:31Z
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.0

[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 _ usr = addRel ("address", CompleteLink $ mkAddrLink $ addressId usr) $ wrap usr
    where
      mkAddrLink = safeLink (Proxy @AddressGetOne) (Proxy @AddressGetOne)

Further we define our API as usual:

type Api = UserApi :<|> AddressApi

type UserApi = UserGetOne :<|> UserGetAll :<|> UserGetAllCool
type UserGetOne     = "api" :> "user" :> Capture "id" Int :> Get '[JSON] User
type UserGetAll     = "api" :> "user" :> 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 1 1000
instance HasHandler UserGetAll where
  getHandler _ _ = return [User 1 1 1000, User 2 2 2000, User 42 3 3000]
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.

The complete example can be found here.

Contact information

Contributions, critics and bug reports are welcome!

Please feel free to contact me through GitHub.