servant-0.8.1: A family of combinators for defining webservices APIs

Safe HaskellNone
LanguageHaskell2010

Servant.Utils.Links

Contents

Description

Type safe generation of internal links.

Given an API with a few endpoints:

>>> :set -XDataKinds -XTypeFamilies -XTypeOperators
>>> import Servant.API
>>> import Servant.Utils.Links
>>> import Data.Proxy
>>> 
>>> 
>>> 
>>> type Hello = "hello" :> Get '[JSON] Int
>>> type Bye   = "bye"   :> QueryParam "name" String :> Delete '[JSON] NoContent
>>> type API   = Hello :<|> Bye
>>> let api = Proxy :: Proxy API

It is possible to generate links that are guaranteed to be within API with safeLink. The first argument to safeLink is a type representing the API you would like to restrict links to. The second argument is the destination endpoint you would like the link to point to, this will need to end with a verb like GET or POST. Further arguments may be required depending on the type of the endpoint. If everything lines up you will get a URI out the other end.

You may omit QueryParams and the like should you not want to provide them, but types which form part of the URL path like Capture must be included. The reason you may want to omit QueryParams is that safeLink is a bit magical: if parameters are included that could take input it will return a function that accepts that input and generates a link. This is best shown with an example. Here, a link is generated with no parameters:

>>> let hello = Proxy :: Proxy ("hello" :> Get '[JSON] Int)
>>> print (safeLink api hello :: URI)
hello

If the API has an endpoint with parameters then we can generate links with or without those:

>>> let with = Proxy :: Proxy ("bye" :> QueryParam "name" String :> Delete '[JSON] NoContent)
>>> print $ safeLink api with (Just "Hubert")
bye?name=Hubert
>>> let without = Proxy :: Proxy ("bye" :> Delete '[JSON] NoContent)
>>> print $ safeLink api without
bye

If you would like create a helper for generating links only within that API, you can partially apply safeLink if you specify a correct type signature like so:

>>> :set -XConstraintKinds
>>> :{
>>> let apiLink :: (IsElem endpoint API, HasLink endpoint)
>>> => Proxy endpoint -> MkLink endpoint
>>> apiLink = safeLink api
>>> :}

Attempting to construct a link to an endpoint that does not exist in api will result in a type error like this:

>>> let bad_link = Proxy :: Proxy ("hello" :> Delete '[JSON] NoContent)
>>> safeLink api bad_link
...
...Could not deduce...
...

This error is essentially saying that the type family couldn't find bad_link under api after trying the open (but empty) type family IsElem' as a last resort.

Synopsis

Building and using safe links

Note that URI is Network.URI.URI from the network-uri package.

safeLink Source #

Arguments

:: (IsElem endpoint api, HasLink endpoint) 
=> Proxy api

The whole API that this endpoint is a part of

-> Proxy endpoint

The API endpoint you would like to point to

-> MkLink endpoint 

Create a valid (by construction) relative URI with query params.

This function will only typecheck if endpoint is part of the API api

data URI :: * #

Represents a general universal resource identifier using its component parts.

For example, for the URI

  foo://anonymous@www.haskell.org:42/ghc?query#frag

the components are:

Constructors

URI 

Fields

Instances

Eq URI 

Methods

(==) :: URI -> URI -> Bool #

(/=) :: URI -> URI -> Bool #

Data URI 

Methods

gfoldl :: (forall d b. Data d => c (d -> b) -> d -> c b) -> (forall g. g -> c g) -> URI -> c URI #

gunfold :: (forall b r. Data b => c (b -> r) -> c r) -> (forall r. r -> c r) -> Constr -> c URI #

toConstr :: URI -> Constr #

dataTypeOf :: URI -> DataType #

dataCast1 :: Typeable (* -> *) t => (forall d. Data d => c (t d)) -> Maybe (c URI) #

dataCast2 :: Typeable (* -> * -> *) t => (forall d e. (Data d, Data e) => c (t d e)) -> Maybe (c URI) #

gmapT :: (forall b. Data b => b -> b) -> URI -> URI #

gmapQl :: (r -> r' -> r) -> r -> (forall d. Data d => d -> r') -> URI -> r #

gmapQr :: (r' -> r -> r) -> r -> (forall d. Data d => d -> r') -> URI -> r #

gmapQ :: (forall d. Data d => d -> u) -> URI -> [u] #

gmapQi :: Int -> (forall d. Data d => d -> u) -> URI -> u #

gmapM :: Monad m => (forall d. Data d => d -> m d) -> URI -> m URI #

gmapMp :: MonadPlus m => (forall d. Data d => d -> m d) -> URI -> m URI #

gmapMo :: MonadPlus m => (forall d. Data d => d -> m d) -> URI -> m URI #

Ord URI 

Methods

compare :: URI -> URI -> Ordering #

(<) :: URI -> URI -> Bool #

(<=) :: URI -> URI -> Bool #

(>) :: URI -> URI -> Bool #

(>=) :: URI -> URI -> Bool #

max :: URI -> URI -> URI #

min :: URI -> URI -> URI #

Show URI 

Methods

showsPrec :: Int -> URI -> ShowS #

show :: URI -> String #

showList :: [URI] -> ShowS #

Generic URI 

Associated Types

type Rep URI :: * -> * #

Methods

from :: URI -> Rep URI x #

to :: Rep URI x -> URI #

NFData URI 

Methods

rnf :: URI -> () #

type Rep URI 

Adding custom types

class HasLink endpoint where Source #

Construct a toLink for an endpoint.

Minimal complete definition

toLink

Associated Types

type MkLink endpoint Source #

Methods

toLink :: Proxy endpoint -> Link -> MkLink endpoint Source #

data Link Source #

A safe link datatype. The only way of constructing a Link is using safeLink, which means any Link is guaranteed to be part of the mentioned API.

type family IsElem' a s :: Constraint Source #

You may use this type family to tell the type checker that your custom type may be skipped as part of a link. This is useful for things like QueryParam that are optional in a URI and do not affect them if they are omitted.

>>> data CustomThing
>>> type instance IsElem' e (CustomThing :> s) = IsElem e s

Note that IsElem is called, which will mutually recurse back to IsElem' if it exhausts all other options again.

Once you have written a HasLink instance for CustomThing you are ready to go.

Illustrative exports

type family IsElem endpoint api :: Constraint where ... Source #

Closed type family, check if endpoint is within api

Equations

IsElem e (sa :<|> sb) = Or (IsElem e sa) (IsElem e sb) 
IsElem (e :> sa) (e :> sb) = IsElem sa sb 
IsElem sa (Header sym x :> sb) = IsElem sa sb 
IsElem sa (ReqBody y x :> sb) = IsElem sa sb 
IsElem (Capture z y :> sa) (Capture x y :> sb) = IsElem sa sb 
IsElem (CaptureAll z y :> sa) (CaptureAll x y :> sb) = IsElem sa sb 
IsElem sa (QueryParam x y :> sb) = IsElem sa sb 
IsElem sa (QueryParams x y :> sb) = IsElem sa sb 
IsElem sa (QueryFlag x :> sb) = IsElem sa sb 
IsElem (Verb m s ct typ) (Verb m s ct' typ) = IsSubList ct ct' 
IsElem e e = () 
IsElem e a = IsElem' e a 

type family Or (a :: Constraint) (b :: Constraint) :: Constraint where ... Source #

If either a or b produce an empty constraint, produce an empty constraint.

Equations

Or () b = () 
Or a () = ()