Flip safeLink and rename link to toLink

This commit is contained in:
Christian Marie 2015-01-30 11:03:48 +11:00
parent 12cf4e02ab
commit d03788ee21
3 changed files with 72 additions and 59 deletions

View file

@ -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 ( safeLink, URI(..) ) import Servant.Utils.Links ( safeLink, URI(..), IsElem, IsElem', HasLink(..) )

View file

@ -25,12 +25,12 @@
-- >>> let api = Proxy :: Proxy API -- >>> let api = Proxy :: Proxy API
-- --
-- It is possible to generate links that are guaranteed to be within 'API' with -- 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 -- 'safeLink'. The first argument to 'safeLink' is a type representing the API
-- endpoint you would like to point to. This will need to end in a verb like -- you would like to restrict links to. The second argument is the destination
-- Get, or Post. The second argument is the API in which you would like to -- endpoint you would like the link to point to, this will need to end with a
-- ensure the endpoint is within. Further arguments be required depending on -- verb like GET or POST. Further arguments may be required depending on the
-- the type of the endpoint. If everything lines up you will get a 'URI' out -- type of the endpoint. If everything lines up you will get a 'URI' out the
-- the other end. -- other end.
-- --
-- You may omit 'QueryParam's and the like should you not want to provide them, -- You may omit 'QueryParam's and the like should you not want to provide them,
-- but types which form part of the URL path like 'Capture' must be included. -- but types which form part of the URL path like 'Capture' must be included.
@ -40,35 +40,46 @@
-- with an example. Here, a link is generated with no parameters: -- with an example. Here, a link is generated with no parameters:
-- --
-- >>> let hello = Proxy :: Proxy ("hello" :> Get Int) -- >>> let hello = Proxy :: Proxy ("hello" :> Get Int)
-- >>> print (safeLink hello api :: URI) -- >>> print (safeLink api hello :: URI)
-- hello -- hello
-- --
-- If the API has an endpoint with parameters then we can generate links with -- If the API has an endpoint with parameters then we can generate links with
-- or without those: -- or without those:
-- --
-- >>> let with = Proxy :: Proxy ("bye" :> QueryParam "name" String :> Delete) -- >>> let with = Proxy :: Proxy ("bye" :> QueryParam "name" String :> Delete)
-- >>> print $ safeLink with api "Hubert" -- >>> print $ safeLink api with "Hubert"
-- bye?name=Hubert -- bye?name=Hubert
-- --
-- >>> let without = Proxy :: Proxy ("bye" :> Delete) -- >>> let without = Proxy :: Proxy ("bye" :> Delete)
-- >>> print $ safeLink without api -- >>> print $ safeLink api without
-- bye -- 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 -- Attempting to construct a link to an endpoint that does not exist in api
-- will result in a type error like this: -- will result in a type error like this:
-- --
-- >>> let bad_link = Proxy :: Proxy ("hello" :> Delete) -- >>> let bad_link = Proxy :: Proxy ("hello" :> Delete)
-- >>> safeLink bad_link api -- >>> safeLink api bad_link
-- <BLANKLINE> -- <BLANKLINE>
-- <interactive>:56:1: -- <interactive>:64:1:
-- Could not deduce (Or -- Could not deduce (Or
-- (IsElem' Delete (Get Int)) -- (IsElem' Delete (Get Int))
-- (IsElem' -- (IsElem'
-- ("hello" :> Delete) -- ("hello" :> Delete)
-- ("bye" :> (QueryParam "name" String :> Delete)))) -- ("bye" :> (QueryParam "name" String :> Delete))))
-- arising from a use of safeLink -- arising from a use of safeLink
-- In the expression: safeLink bad_link api -- In the expression: safeLink api bad_link
-- In an equation for it: it = safeLink bad_link api -- In an equation for it: it = safeLink api bad_link
-- --
-- This error is essentially saying that the type family couldn't find -- This error is essentially saying that the type family couldn't find
-- bad_link under api after trying the open (but empty) type family -- bad_link under api after trying the open (but empty) type family
@ -213,23 +224,23 @@ escape = escapeURIString isUnreserved
-- This function will only typecheck if `endpoint` is part of the API `api` -- This function will only typecheck if `endpoint` is part of the API `api`
safeLink safeLink
:: forall endpoint api. (IsElem endpoint api, HasLink endpoint) :: forall endpoint api. (IsElem endpoint api, HasLink endpoint)
=> Proxy endpoint -- ^ The API endpoint you would like to point to => Proxy api -- ^ The whole API that this endpoint is a part of
-> 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 -> MkLink endpoint
safeLink endpoint _ = link endpoint (Link mempty mempty) safeLink _ endpoint = toLink endpoint (Link mempty mempty)
-- | Construct a link for an endpoint. -- | Construct a toLink for an endpoint.
class HasLink endpoint where class HasLink endpoint where
type MkLink endpoint type MkLink endpoint
link :: Proxy endpoint -- ^ The API endpoint you would like to point to toLink :: Proxy endpoint -- ^ The API endpoint you would like to point to
-> Link -> Link
-> MkLink endpoint -> MkLink endpoint
-- Naked symbol instance -- Naked symbol instance
instance (KnownSymbol sym, HasLink sub) => HasLink (sym :> sub) where instance (KnownSymbol sym, HasLink sub) => HasLink (sym :> sub) where
type MkLink (sym :> sub) = MkLink sub type MkLink (sym :> sub) = MkLink sub
link _ = toLink _ =
link (Proxy :: Proxy sub) . addSegment seg toLink (Proxy :: Proxy sub) . addSegment seg
where where
seg = symbolVal (Proxy :: Proxy sym) seg = symbolVal (Proxy :: Proxy sym)
@ -238,8 +249,8 @@ instance (KnownSymbol sym, HasLink sub) => HasLink (sym :> sub) where
instance (KnownSymbol sym, ToText v, HasLink sub) instance (KnownSymbol sym, ToText v, HasLink sub)
=> HasLink (QueryParam sym v :> sub) where => HasLink (QueryParam sym v :> sub) where
type MkLink (QueryParam sym v :> sub) = v -> MkLink sub type MkLink (QueryParam sym v :> sub) = v -> MkLink sub
link _ l v = toLink _ l v =
link (Proxy :: Proxy sub) toLink (Proxy :: Proxy sub)
(addQueryParam (SingleParam k (toText v)) l) (addQueryParam (SingleParam k (toText v)) l)
where where
k :: String k :: String
@ -248,8 +259,8 @@ instance (KnownSymbol sym, ToText v, HasLink sub)
instance (KnownSymbol sym, ToText v, HasLink sub) instance (KnownSymbol sym, ToText v, HasLink sub)
=> HasLink (QueryParams sym v :> sub) where => HasLink (QueryParams sym v :> sub) where
type MkLink (QueryParams sym v :> sub) = [v] -> MkLink sub type MkLink (QueryParams sym v :> sub) = [v] -> MkLink sub
link _ l = toLink _ l =
link (Proxy :: Proxy sub) . toLink (Proxy :: Proxy sub) .
foldl' (\l' v -> addQueryParam (ArrayElemParam k (toText v)) l') l foldl' (\l' v -> addQueryParam (ArrayElemParam k (toText v)) l') l
where where
k = symbolVal (Proxy :: Proxy sym) k = symbolVal (Proxy :: Proxy sym)
@ -257,10 +268,10 @@ instance (KnownSymbol sym, ToText v, HasLink sub)
instance (KnownSymbol sym, HasLink sub) instance (KnownSymbol sym, HasLink sub)
=> HasLink (QueryFlag sym :> sub) where => HasLink (QueryFlag sym :> sub) where
type MkLink (QueryFlag sym :> sub) = Bool -> MkLink sub type MkLink (QueryFlag sym :> sub) = Bool -> MkLink sub
link _ l False = toLink _ l False =
link (Proxy :: Proxy sub) l toLink (Proxy :: Proxy sub) l
link _ l True = toLink _ l True =
link (Proxy :: Proxy sub) $ addQueryParam (FlagParam k) l toLink (Proxy :: Proxy sub) $ addQueryParam (FlagParam k) l
where where
k = symbolVal (Proxy :: Proxy sym) k = symbolVal (Proxy :: Proxy sym)
@ -268,8 +279,8 @@ instance (KnownSymbol sym, HasLink sub)
instance (KnownSymbol sym, ToText v, HasLink sub) instance (KnownSymbol sym, ToText v, HasLink sub)
=> HasLink (MatrixParam sym v :> sub) where => HasLink (MatrixParam sym v :> sub) where
type MkLink (MatrixParam sym v :> sub) = v -> MkLink sub type MkLink (MatrixParam sym v :> sub) = v -> MkLink sub
link _ l v = toLink _ l v =
link (Proxy :: Proxy sub) $ toLink (Proxy :: Proxy sub) $
addMatrixParam (SingleParam k (toText v)) l addMatrixParam (SingleParam k (toText v)) l
where where
k = symbolVal (Proxy :: Proxy sym) k = symbolVal (Proxy :: Proxy sym)
@ -277,8 +288,8 @@ instance (KnownSymbol sym, ToText v, HasLink sub)
instance (KnownSymbol sym, ToText v, HasLink sub) instance (KnownSymbol sym, ToText v, HasLink sub)
=> HasLink (MatrixParams sym v :> sub) where => HasLink (MatrixParams sym v :> sub) where
type MkLink (MatrixParams sym v :> sub) = [v] -> MkLink sub type MkLink (MatrixParams sym v :> sub) = [v] -> MkLink sub
link _ l = toLink _ l =
link (Proxy :: Proxy sub) . toLink (Proxy :: Proxy sub) .
foldl' (\l' v -> addMatrixParam (ArrayElemParam k (toText v)) l') l foldl' (\l' v -> addMatrixParam (ArrayElemParam k (toText v)) l') l
where where
k = symbolVal (Proxy :: Proxy sym) k = symbolVal (Proxy :: Proxy sym)
@ -286,42 +297,42 @@ instance (KnownSymbol sym, ToText v, HasLink sub)
instance (KnownSymbol sym, HasLink sub) instance (KnownSymbol sym, HasLink sub)
=> HasLink (MatrixFlag sym :> sub) where => HasLink (MatrixFlag sym :> sub) where
type MkLink (MatrixFlag sym :> sub) = Bool -> MkLink sub type MkLink (MatrixFlag sym :> sub) = Bool -> MkLink sub
link _ l False = toLink _ l False =
link (Proxy :: Proxy sub) l toLink (Proxy :: Proxy sub) l
link _ l True = toLink _ l True =
link (Proxy :: Proxy sub) $ addMatrixParam (FlagParam k) l toLink (Proxy :: Proxy sub) $ addMatrixParam (FlagParam k) l
where where
k = symbolVal (Proxy :: Proxy sym) k = symbolVal (Proxy :: Proxy sym)
-- Misc instances -- Misc instances
instance HasLink sub => HasLink (ReqBody a :> sub) where instance HasLink sub => HasLink (ReqBody a :> sub) where
type MkLink (ReqBody a :> sub) = MkLink sub type MkLink (ReqBody a :> sub) = MkLink sub
link _ = link (Proxy :: Proxy sub) toLink _ = toLink (Proxy :: Proxy sub)
instance (ToText v, HasLink sub) instance (ToText v, HasLink sub)
=> HasLink (Capture sym v :> sub) where => HasLink (Capture sym v :> sub) where
type MkLink (Capture sym v :> sub) = v -> MkLink sub type MkLink (Capture sym v :> sub) = v -> MkLink sub
link _ l v = toLink _ l v =
link (Proxy :: Proxy sub) $ toLink (Proxy :: Proxy sub) $
addSegment (escape . unpack $ toText v) l addSegment (escape . unpack $ toText v) l
-- Verb (terminal) instances -- Verb (terminal) instances
instance HasLink (Get r) where instance HasLink (Get r) where
type MkLink (Get r) = URI type MkLink (Get r) = URI
link _ = linkURI toLink _ = linkURI
instance HasLink (Post r) where instance HasLink (Post r) where
type MkLink (Post r) = URI type MkLink (Post r) = URI
link _ = linkURI toLink _ = linkURI
instance HasLink (Put r) where instance HasLink (Put r) where
type MkLink (Put r) = URI type MkLink (Put r) = URI
link _ = linkURI toLink _ = linkURI
instance HasLink Delete where instance HasLink Delete where
type MkLink Delete = URI type MkLink Delete = URI
link _ = linkURI toLink _ = linkURI
instance HasLink Raw where instance HasLink Raw where
type MkLink Raw = URI type MkLink Raw = URI
link _ = linkURI toLink _ = linkURI

View file

@ -1,6 +1,7 @@
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-} {-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ConstraintKinds #-}
module Servant.Utils.LinksSpec where module Servant.Utils.LinksSpec where
@ -31,8 +32,9 @@ 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
api :: Proxy TestApi apiLink :: (IsElem endpoint TestApi, HasLink endpoint)
api = Proxy => Proxy endpoint -> MkLink endpoint
apiLink = safeLink (Proxy :: Proxy TestApi)
-- | Convert a link to a URI and ensure that this maps to the given string -- | Convert a link to a URI and ensure that this maps to the given string
-- given string -- given string
@ -44,35 +46,35 @@ spec :: Spec
spec = describe "Servant.Utils.Links" $ do spec = describe "Servant.Utils.Links" $ do
it "Generates correct links for capture query and matrix params" $ do it "Generates correct links for capture query and matrix params" $ do
let l1 = Proxy :: Proxy ("hello" :> Capture "name" String :> Delete) let l1 = Proxy :: Proxy ("hello" :> Capture "name" String :> Delete)
safeLink l1 api "hi" `shouldBeURI` "hello/hi" apiLink l1 "hi" `shouldBeURI` "hello/hi"
let l2 = Proxy :: Proxy ("hello" :> Capture "name" String let l2 = Proxy :: Proxy ("hello" :> Capture "name" String
:> QueryParam "capital" Bool :> QueryParam "capital" Bool
:> Delete) :> Delete)
safeLink l2 api "bye" True `shouldBeURI` "hello/bye?capital=true" apiLink l2 "bye" True `shouldBeURI` "hello/bye?capital=true"
let l3 = Proxy :: Proxy ("parent" :> MatrixParams "name" String let l3 = Proxy :: Proxy ("parent" :> MatrixParams "name" String
:> "child" :> "child"
:> MatrixParam "gender" String :> MatrixParam "gender" String
:> Get String) :> Get String)
safeLink l3 api ["Hubert?x=;&", "Cumberdale"] "Edward?" apiLink l3 ["Hubert?x=;&", "Cumberdale"] "Edward?"
`shouldBeURI` "parent;name[]=Hubert%3Fx%3D%3B%26;\ `shouldBeURI` "parent;name[]=Hubert%3Fx%3D%3B%26;\
\name[]=Cumberdale/child;gender=Edward%3F" \name[]=Cumberdale/child;gender=Edward%3F"
it "Generates correct links for query and matrix flags" $ do it "Generates correct links for query and matrix flags" $ do
let l1 = Proxy :: Proxy ("balls" :> QueryFlag "bouncy" let l1 = Proxy :: Proxy ("balls" :> QueryFlag "bouncy"
:> QueryFlag "fast" :> Delete) :> QueryFlag "fast" :> Delete)
safeLink l1 api True True `shouldBeURI` "balls?bouncy&fast" apiLink l1 True True `shouldBeURI` "balls?bouncy&fast"
safeLink l1 api False True `shouldBeURI` "balls?fast" apiLink l1 False True `shouldBeURI` "balls?fast"
let l2 = Proxy :: Proxy ("ducks" :> MatrixFlag "yellow" let l2 = Proxy :: Proxy ("ducks" :> MatrixFlag "yellow"
:> MatrixFlag "loud" :> Delete) :> MatrixFlag "loud" :> Delete)
safeLink l2 api True True `shouldBeURI` "ducks;yellow;loud" apiLink l2 True True `shouldBeURI` "ducks;yellow;loud"
safeLink l2 api False True `shouldBeURI` "ducks;loud" apiLink l2 False True `shouldBeURI` "ducks;loud"
it "Generates correct links for all of the verbs" $ do it "Generates correct links for all of the verbs" $ do
safeLink (Proxy :: Proxy ("get" :> Get ())) api `shouldBeURI` "get" apiLink (Proxy :: Proxy ("get" :> Get ())) `shouldBeURI` "get"
safeLink (Proxy :: Proxy ("put" :> Put ())) api `shouldBeURI` "put" apiLink (Proxy :: Proxy ("put" :> Put ())) `shouldBeURI` "put"
safeLink (Proxy :: Proxy ("post" :> Post ())) api `shouldBeURI` "post" apiLink (Proxy :: Proxy ("post" :> Post ())) `shouldBeURI` "post"
safeLink (Proxy :: Proxy ("delete" :> Delete)) api `shouldBeURI` "delete" apiLink (Proxy :: Proxy ("delete" :> Delete)) `shouldBeURI` "delete"
safeLink (Proxy :: Proxy ("raw" :> Raw)) api `shouldBeURI` "raw" apiLink (Proxy :: Proxy ("raw" :> Raw)) `shouldBeURI` "raw"