Merge pull request #10 from anchor/wip-new-safe-links

New safe link API
This commit is contained in:
Christian Marie 2015-01-31 10:14:59 +11:00
commit b27e256b41
6 changed files with 362 additions and 123 deletions

View file

@ -3,6 +3,13 @@ language: haskell
ghc: ghc:
- 7.8 - 7.8
script:
- cabal configure --enable-tests && cabal build && cabal test
- cabal check
- cabal sdist
- cabal install doctest
- ./test-docs.sh
notifications: notifications:
irc: irc:
channels: channels:

View file

@ -47,6 +47,7 @@ library
, template-haskell , template-haskell
, parsec >= 3.1 , parsec >= 3.1
, string-conversions >= 0.3 , string-conversions >= 0.3
, network-uri >= 2.6
hs-source-dirs: src hs-source-dirs: src
default-language: Haskell2010 default-language: Haskell2010
ghc-options: -Wall ghc-options: -Wall

View file

@ -35,7 +35,7 @@ module Servant.API (
-- * Utilities -- * Utilities
-- | QuasiQuotes for endpoints -- | QuasiQuotes for endpoints
module Servant.QQ, module Servant.QQ,
-- | Type-safe internal URLs -- | Type-safe internal URIs
module Servant.Utils.Links, module Servant.Utils.Links,
) where ) where
@ -52,4 +52,4 @@ import Servant.API.Raw ( Raw )
import Servant.API.ReqBody ( ReqBody ) import Servant.API.ReqBody ( ReqBody )
import Servant.API.Sub ( (:>)(..) ) import Servant.API.Sub ( (:>)(..) )
import Servant.QQ ( sitemap ) import Servant.QQ ( sitemap )
import Servant.Utils.Links ( mkLink ) import Servant.Utils.Links ( safeLink, URI(..), IsElem, IsElem', HasLink(..) )

View file

@ -1,4 +1,5 @@
{-# LANGUAGE PolyKinds #-} {-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
@ -6,77 +7,146 @@
{-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
-- | Type safe internal links.
-- | Type safe generation of internal links.
-- --
-- Provides the function 'mkLink': -- Given an API with a few endpoints:
-- --
-- @ -- >>> :set -XDataKinds -XTypeFamilies -XTypeOperators
-- type API = Proxy ("hello" :> Get Int -- >>> import Servant.API
-- :<|> "bye" :> QueryParam "name" String :> Post Bool) -- >>> import Servant.Utils.Links
-- >>> import Data.Proxy
-- >>>
-- >>>
-- >>>
-- >>> type Hello = "hello" :> Get Int
-- >>> type Bye = "bye" :> QueryParam "name" String :> Delete
-- >>> type API = Hello :<|> Bye
-- >>> let api = Proxy :: Proxy API
-- --
-- api :: API -- It is possible to generate links that are guaranteed to be within 'API' with
-- api = proxy -- '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.
-- --
-- link1 :: Proxy ("hello" :> Get Int) -- You may omit 'QueryParam's and the like should you not want to provide them,
-- link1 = proxy -- but types which form part of the URL path like 'Capture' must be included.
-- The reason you may want to omit 'QueryParam's 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:
-- --
-- link2 :: Proxy ("hello" :> Delete) -- >>> let hello = Proxy :: Proxy ("hello" :> Get Int)
-- link2 = proxy -- >>> print (safeLink api hello :: URI)
-- hello
-- --
-- mkLink link1 API -- typechecks, returns 'Link "/hello"' -- If the API has an endpoint with parameters then we can generate links with
-- or without those:
-- --
-- mkLink link2 API -- doesn't typecheck -- >>> let with = Proxy :: Proxy ("bye" :> QueryParam "name" String :> Delete)
-- @ -- >>> print $ safeLink api with "Hubert"
-- bye?name=Hubert
-- --
-- That is, 'mkLink' takes two arguments, a link proxy and a sitemap, and -- >>> let without = Proxy :: Proxy ("bye" :> Delete)
-- returns a 'Link', but only typechecks if the link proxy is a valid link, -- >>> print $ safeLink api without
-- and part of the sitemap. -- bye
-- --
-- __N.B.:__ 'mkLink' assumes a capture matches any string (without slashes). -- 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)
-- >>> safeLink api bad_link
-- <BLANKLINE>
-- <interactive>:64:1:
-- Could not deduce (Or
-- (IsElem' Delete (Get Int))
-- (IsElem'
-- ("hello" :> Delete)
-- ("bye" :> (QueryParam "name" String :> Delete))))
-- arising from a use of safeLink
-- In the expression: safeLink api bad_link
-- In an equation for it: it = safeLink api bad_link
--
-- 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.
module Servant.Utils.Links ( module Servant.Utils.Links (
-- * Link and mkLink -- * Building and using safe links
-- | The only end-user utilities --
mkLink -- | Note that 'URI' is Network.URI.URI from the network-uri package.
, Link safeLink
-- * Internal , URI(..)
-- | These functions will likely only be of interest if you are writing -- * Adding custom types
-- more API combinators and would like to extend the behavior of , HasLink(..)
-- 'mkLink' , linkURI
, ValidLinkIn() , Link
, VLinkHelper(..) , IsElem'
, IsElem -- * Illustrative exports
, IsLink , IsElem
)where , Or
) where
import Data.List
import Data.Proxy ( Proxy(..) ) import Data.Proxy ( Proxy(..) )
import GHC.TypeLits ( KnownSymbol, Symbol, symbolVal ) import Data.Text (Text, unpack)
import Data.Monoid ( Monoid(..), (<>) )
import Network.URI ( URI(..), escapeURIString, isUnreserved )
import GHC.TypeLits ( KnownSymbol, symbolVal )
import GHC.Exts(Constraint)
import Servant.Common.Text
import Servant.API.Capture ( Capture ) import Servant.API.Capture ( Capture )
import Servant.API.ReqBody ( ReqBody ) import Servant.API.ReqBody ( ReqBody )
import Servant.API.QueryParam ( QueryParam, QueryParams, QueryFlag ) import Servant.API.QueryParam ( QueryParam, QueryParams, QueryFlag )
import Servant.API.MatrixParam ( MatrixParam, MatrixParams, MatrixFlag ) import Servant.API.MatrixParam ( MatrixParam, MatrixParams, MatrixFlag )
import Servant.API.Header ( Header )
import Servant.API.Get ( Get ) import Servant.API.Get ( Get )
import Servant.API.Post ( Post ) import Servant.API.Post ( Post )
import Servant.API.Put ( Put ) import Servant.API.Put ( Put )
import Servant.API.Delete ( Delete ) import Servant.API.Delete ( Delete )
import Servant.API.Sub ( type (:>) ) import Servant.API.Sub ( type (:>) )
import Servant.API.Raw ( Raw )
import Servant.API.Alternative ( type (:<|>) ) import Servant.API.Alternative ( type (:<|>) )
-- | If either a or b produce an empty constraint, produce an empty constraint.
type family Or (a :: Constraint) (b :: Constraint) :: Constraint where
Or () b = ()
Or a () = ()
type family Or a b where -- | You may use this type family to tell the type checker that your custom type
Or 'False 'False = 'False -- may be skipped as part of a link. This is useful for things like
Or 'True b = 'True -- 'QueryParam' that are optional in a URI and do not affect them if they are
Or a 'True = 'True -- 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.
type family IsElem' a s :: Constraint
type family And a b where -- | Closed type family, check if endpoint is within api
And 'True 'True = 'True type family IsElem endpoint api :: Constraint where
And a 'False = 'False
And 'False b = 'False
type family IsElem a s where
IsElem e (sa :<|> sb) = Or (IsElem e sa) (IsElem e sb) IsElem e (sa :<|> sb) = Or (IsElem e sa) (IsElem e sb)
IsElem (e :> sa) (e :> sb) = IsElem sa sb IsElem (e :> sa) (e :> sb) = IsElem sa sb
IsElem (e :> sa) (Capture x y :> sb) = IsElem sa sb IsElem sa (Header x :> sb) = IsElem sa sb
IsElem sa (ReqBody x :> sb) = IsElem sa sb IsElem sa (ReqBody x :> sb) = IsElem sa sb
IsElem sa (QueryParam 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 (QueryParams x y :> sb) = IsElem sa sb
@ -84,48 +154,185 @@ type family IsElem a s where
IsElem sa (MatrixParam x y :> sb) = IsElem sa sb IsElem sa (MatrixParam x y :> sb) = IsElem sa sb
IsElem sa (MatrixParams x y :> sb) = IsElem sa sb IsElem sa (MatrixParams x y :> sb) = IsElem sa sb
IsElem sa (MatrixFlag x :> sb) = IsElem sa sb IsElem sa (MatrixFlag x :> sb) = IsElem sa sb
IsElem e e = 'True IsElem e e = ()
IsElem e a = 'False IsElem e a = IsElem' e a
type family IsLink'' l where
IsLink'' (e :> Get x) = IsLink' e
IsLink'' (e :> Post x) = IsLink' e
IsLink'' (e :> Put x) = IsLink' e
IsLink'' (e :> Delete) = IsLink' e
IsLink'' a = 'False
type family IsLink' e where
IsLink' (f :: Symbol) = 'True
type family IsLink e where
IsLink (a :> b) = Or (And (IsLink' a) (IsLink'' b))
(IsLink'' (a :> b))
-- | The 'ValidLinkIn f s' constraint holds when 's' is an API that
-- contains 'f', and 'f' is a link.
class ValidLinkIn f s where
mkLink :: f -> s -> Link -- ^ This function will only typecheck if `f`
-- is an URI within `s`
instance ( IsElem f s ~ 'True
, IsLink f ~ 'True
, VLinkHelper f) => ValidLinkIn f s where
mkLink _ _ = Link (vlh (Proxy :: Proxy f))
-- | A safe link datatype. -- | A safe link datatype.
-- The only way of constructing a 'Link' is using 'mkLink', which means any -- The only way of constructing a 'Link' is using 'safeLink', which means any
-- 'Link' is guaranteed to be part of the mentioned API. -- 'Link' is guaranteed to be part of the mentioned API.
data Link = Link String deriving Show data Link = Link
{ _segments :: [String] -- ^ Segments of "foo/bar" would be ["foo", "bar"]
, _queryParams :: [Param Query]
} deriving Show
class VLinkHelper f where
vlh :: forall proxy. proxy f -> String
instance (KnownSymbol s, VLinkHelper e) => VLinkHelper (s :> e) where -- Phantom types for Param
vlh _ = "/" ++ symbolVal (Proxy :: Proxy s) ++ vlh (Proxy :: Proxy e) data Matrix
data Query
instance VLinkHelper (Get x) where -- | Query/Matrix param
vlh _ = "" data Param a
= SingleParam String Text
| ArrayElemParam String Text
| FlagParam String
deriving Show
instance VLinkHelper (Post x) where addSegment :: String -> Link -> Link
vlh _ = "" addSegment seg l = l { _segments = _segments l <> [seg] }
addQueryParam :: Param Query -> Link -> Link
addQueryParam qp l =
l { _queryParams = _queryParams l <> [qp] }
-- Not particularly efficient for many updates. Something to optimise if it's
-- a problem.
addMatrixParam :: Param Matrix -> Link -> Link
addMatrixParam param l = l { _segments = f (_segments l) }
where
f [] = []
f xs = init xs <> [g (last xs)]
-- Modify the segment at the "top" of the stack
g :: String -> String
g seg =
case param of
SingleParam k v -> seg <> ";" <> k <> "=" <> escape (unpack v)
ArrayElemParam k v -> seg <> ";" <> k <> "[]=" <> escape (unpack v)
FlagParam k -> seg <> ";" <> k
linkURI :: Link -> URI
linkURI (Link segments q_params) =
URI mempty -- No scheme (relative)
Nothing -- Or authority (relative)
(intercalate "/" segments)
(makeQueries q_params) mempty
where
makeQueries :: [Param Query] -> String
makeQueries [] = ""
makeQueries xs =
"?" <> intercalate "&" (fmap makeQuery xs)
makeQuery :: Param Query -> String
makeQuery (ArrayElemParam k v) = escape k <> "[]=" <> escape (unpack v)
makeQuery (SingleParam k v) = escape k <> "=" <> escape (unpack v)
makeQuery (FlagParam k) = escape k
escape :: String -> String
escape = escapeURIString isUnreserved
-- | Create a valid (by construction) relative URI with query params.
--
-- This function will only typecheck if `endpoint` is part of the API `api`
safeLink
:: forall endpoint api. (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
safeLink _ endpoint = toLink endpoint (Link mempty mempty)
-- | Construct a toLink for an endpoint.
class HasLink endpoint where
type MkLink endpoint
toLink :: Proxy endpoint -- ^ The API endpoint you would like to point to
-> Link
-> MkLink endpoint
-- Naked symbol instance
instance (KnownSymbol sym, HasLink sub) => HasLink (sym :> sub) where
type MkLink (sym :> sub) = MkLink sub
toLink _ =
toLink (Proxy :: Proxy sub) . addSegment seg
where
seg = symbolVal (Proxy :: Proxy sym)
-- QueryParam instances
instance (KnownSymbol sym, ToText v, HasLink sub)
=> HasLink (QueryParam sym v :> sub) where
type MkLink (QueryParam sym v :> sub) = v -> MkLink sub
toLink _ l v =
toLink (Proxy :: Proxy sub)
(addQueryParam (SingleParam k (toText v)) l)
where
k :: String
k = symbolVal (Proxy :: Proxy sym)
instance (KnownSymbol sym, ToText v, HasLink sub)
=> HasLink (QueryParams sym v :> sub) where
type MkLink (QueryParams sym v :> sub) = [v] -> MkLink sub
toLink _ l =
toLink (Proxy :: Proxy sub) .
foldl' (\l' v -> addQueryParam (ArrayElemParam k (toText v)) l') l
where
k = symbolVal (Proxy :: Proxy sym)
instance (KnownSymbol sym, HasLink sub)
=> HasLink (QueryFlag sym :> sub) where
type MkLink (QueryFlag sym :> sub) = Bool -> MkLink sub
toLink _ l False =
toLink (Proxy :: Proxy sub) l
toLink _ l True =
toLink (Proxy :: Proxy sub) $ addQueryParam (FlagParam k) l
where
k = symbolVal (Proxy :: Proxy sym)
-- MatrixParam instances
instance (KnownSymbol sym, ToText v, HasLink sub)
=> HasLink (MatrixParam sym v :> sub) where
type MkLink (MatrixParam sym v :> sub) = v -> MkLink sub
toLink _ l v =
toLink (Proxy :: Proxy sub) $
addMatrixParam (SingleParam k (toText v)) l
where
k = symbolVal (Proxy :: Proxy sym)
instance (KnownSymbol sym, ToText v, HasLink sub)
=> HasLink (MatrixParams sym v :> sub) where
type MkLink (MatrixParams sym v :> sub) = [v] -> MkLink sub
toLink _ l =
toLink (Proxy :: Proxy sub) .
foldl' (\l' v -> addMatrixParam (ArrayElemParam k (toText v)) l') l
where
k = symbolVal (Proxy :: Proxy sym)
instance (KnownSymbol sym, HasLink sub)
=> HasLink (MatrixFlag sym :> sub) where
type MkLink (MatrixFlag sym :> sub) = Bool -> MkLink sub
toLink _ l False =
toLink (Proxy :: Proxy sub) l
toLink _ l True =
toLink (Proxy :: Proxy sub) $ addMatrixParam (FlagParam k) l
where
k = symbolVal (Proxy :: Proxy sym)
-- Misc instances
instance HasLink sub => HasLink (ReqBody a :> sub) where
type MkLink (ReqBody a :> sub) = MkLink sub
toLink _ = toLink (Proxy :: Proxy sub)
instance (ToText v, HasLink sub)
=> HasLink (Capture sym v :> sub) where
type MkLink (Capture sym v :> sub) = v -> MkLink sub
toLink _ l v =
toLink (Proxy :: Proxy sub) $
addSegment (escape . unpack $ toText v) l
-- Verb (terminal) instances
instance HasLink (Get r) where
type MkLink (Get r) = URI
toLink _ = linkURI
instance HasLink (Post r) where
type MkLink (Post r) = URI
toLink _ = linkURI
instance HasLink (Put r) where
type MkLink (Put r) = URI
toLink _ = linkURI
instance HasLink Delete where
type MkLink Delete = URI
toLink _ = linkURI
instance HasLink Raw where
type MkLink Raw = URI
toLink _ = linkURI

3
test-docs.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
doctest -isrc -optP-include -optPdist/build/autogen/cabal_macros.h $(find src/ -name '*.hs')

View file

@ -1,59 +1,80 @@
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-} {-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ConstraintKinds #-}
module Servant.Utils.LinksSpec where module Servant.Utils.LinksSpec where
import Test.Hspec ( Spec, it, describe ) import Test.Hspec ( Spec, it, describe, shouldBe, Expectation )
import Data.Proxy ( Proxy(..) )
import Servant.API import Servant.API
( type (:<|>), ReqBody, QueryParam, MatrixParam, MatrixParams, MatrixFlag, Get, Post, Capture, type (:>) )
import Servant.QQSpec ( (~>) )
import Servant.Utils.Links ( IsElem, IsLink )
type TestApi = type TestApi =
"hello" :> Capture "name" String :> QueryParam "capital" Bool :> Get Bool -- Capture and query/matrix params
:<|> "greet" :> ReqBody 'True :> Post Bool "hello" :> Capture "name" String :> QueryParam "capital" Bool :> Delete
:<|> "parent" :> MatrixParams "name" String :> "child" :> MatrixParam "gender" String :> Get String
:<|> "parent" :> MatrixParams "name" String :> "child"
:> MatrixParam "gender" String :> Get String
-- Flags
:<|> "ducks" :> MatrixFlag "yellow" :> MatrixFlag "loud" :> Delete
:<|> "balls" :> QueryFlag "bouncy" :> QueryFlag "fast" :> Delete
-- All of the verbs
:<|> "get" :> Get ()
:<|> "put" :> Put ()
:<|> "post" :> ReqBody 'True :> Post ()
:<|> "delete" :> Header "ponies" :> Delete
:<|> "raw" :> Raw
type TestLink = "hello" :> "hi" :> Get Bool type TestLink = "hello" :> "hi" :> Get Bool
type TestLink2 = "greet" :> Post Bool type TestLink2 = "greet" :> Post Bool
type TestLink3 = "parent" :> "child" :> Get String type TestLink3 = "parent" :> "child" :> Get String
type BadTestLink = "hallo" :> "hi" :> Get Bool apiLink :: (IsElem endpoint TestApi, HasLink endpoint)
type BadTestLink2 = "greet" :> Get Bool => Proxy endpoint -> MkLink endpoint
type BadTestLink3 = "parent" :> "child" :> MatrixFlag "male" :> Get String apiLink = safeLink (Proxy :: Proxy TestApi)
type NotALink = "hello" :> Capture "x" Bool :> Get Bool -- | Convert a link to a URI and ensure that this maps to the given string
type NotALink2 = "hello" :> ReqBody 'True :> Get Bool -- given string
shouldBeURI :: URI -> String -> Expectation
data Proxy x = Proxy shouldBeURI link expected =
class ReflectT (x::Bool) where { reflected :: Proxy x -> Bool } show link `shouldBe` expected
instance ReflectT 'True where { reflected _ = True }
instance ReflectT 'False where { reflected _ = False }
spec :: Spec spec :: Spec
spec = describe "Servant.API.Elem" $ do spec = describe "Servant.Utils.Links" $ do
isElem it "Generates correct links for capture query and matrix params" $ do
isLink let l1 = Proxy :: Proxy ("hello" :> Capture "name" String :> Delete)
apiLink l1 "hi" `shouldBeURI` "hello/hi"
isElem :: Spec let l2 = Proxy :: Proxy ("hello" :> Capture "name" String
isElem = describe "IsElem" $ do :> QueryParam "capital" Bool
it "is True when the first argument is an url within the second" $ do :> Delete)
reflected (Proxy::Proxy (IsElem TestLink TestApi)) ~> True apiLink l2 "bye" True `shouldBeURI` "hello/bye?capital=true"
reflected (Proxy::Proxy (IsElem TestLink2 TestApi)) ~> True
reflected (Proxy::Proxy (IsElem TestLink3 TestApi)) ~> True
it "is False when the first argument is not an url within the second" $ do
reflected (Proxy::Proxy (IsElem BadTestLink TestApi)) ~> False
reflected (Proxy::Proxy (IsElem BadTestLink2 TestApi)) ~> False
reflected (Proxy::Proxy (IsElem BadTestLink3 TestApi)) ~> False
isLink :: Spec let l3 = Proxy :: Proxy ("parent" :> MatrixParams "name" String
isLink = describe "IsLink" $ do :> "child"
it "is True when all Subs are paths and the last is a method" $ do :> MatrixParam "gender" String
reflected (Proxy::Proxy (IsLink TestLink)) ~> True :> Get String)
reflected (Proxy::Proxy (IsLink TestLink2)) ~> True apiLink l3 ["Hubert?x=;&", "Cumberdale"] "Edward?"
reflected (Proxy::Proxy (IsLink TestLink3)) ~> True `shouldBeURI` "parent;name[]=Hubert%3Fx%3D%3B%26;\
it "is False of anything with captures" $ do \name[]=Cumberdale/child;gender=Edward%3F"
reflected (Proxy::Proxy (IsLink NotALink)) ~> False
reflected (Proxy::Proxy (IsLink NotALink2)) ~> False it "Generates correct links for query and matrix flags" $ do
let l1 = Proxy :: Proxy ("balls" :> QueryFlag "bouncy"
:> QueryFlag "fast" :> Delete)
apiLink l1 True True `shouldBeURI` "balls?bouncy&fast"
apiLink l1 False True `shouldBeURI` "balls?fast"
let l2 = Proxy :: Proxy ("ducks" :> MatrixFlag "yellow"
:> MatrixFlag "loud" :> Delete)
apiLink l2 True True `shouldBeURI` "ducks;yellow;loud"
apiLink l2 False True `shouldBeURI` "ducks;loud"
it "Generates correct links for all of the verbs" $ do
apiLink (Proxy :: Proxy ("get" :> Get ())) `shouldBeURI` "get"
apiLink (Proxy :: Proxy ("put" :> Put ())) `shouldBeURI` "put"
apiLink (Proxy :: Proxy ("post" :> Post ())) `shouldBeURI` "post"
apiLink (Proxy :: Proxy ("delete" :> Delete)) `shouldBeURI` "delete"
apiLink (Proxy :: Proxy ("raw" :> Raw)) `shouldBeURI` "raw"