git.iko.soy/typeable/servant
on master help sign in
Merge pull request #10 from anchor/wip-new-safe-links
b27e256Christian Marie
.travis.yml+7
@@ -3,6 +3,13 @@ language: haskell
3ghc:
3ghc:
4 - 7.8
4 - 7.8
5
5
6+script:
7+ - cabal configure --enable-tests && cabal build && cabal test
8+ - cabal check
9+ - cabal sdist
10+ - cabal install doctest
11+ - ./test-docs.sh
12+
6notifications:
13notifications:
7 irc:
14 irc:
8 channels:
15 channels:
servant.cabal+1
@@ -47,6 +47,7 @@ library
47 , template-haskell
47 , template-haskell
48 , parsec >= 3.1
48 , parsec >= 3.1
49 , string-conversions >= 0.3
49 , string-conversions >= 0.3
50+ , network-uri >= 2.6
50 hs-source-dirs: src
51 hs-source-dirs: src
51 default-language: Haskell2010
52 default-language: Haskell2010
52 ghc-options: -Wall
53 ghc-options: -Wall
src/Servant/API.hs+2−2
@@ -35,7 +35,7 @@ module Servant.API (
35 -- * Utilities
35 -- * Utilities
36 -- | QuasiQuotes for endpoints
36 -- | QuasiQuotes for endpoints
37 module Servant.QQ,
37 module Servant.QQ,
38− -- | Type-safe internal URLs
38+ -- | Type-safe internal URIs
39 module Servant.Utils.Links,
39 module Servant.Utils.Links,
40 ) where
40 ) where
41
41
@@ -52,4 +52,4 @@ import Servant.API.Raw ( Raw )
52import Servant.API.ReqBody ( ReqBody )
52import Servant.API.ReqBody ( ReqBody )
53import Servant.API.Sub ( (:>)(..) )
53import Servant.API.Sub ( (:>)(..) )
54import Servant.QQ ( sitemap )
54import Servant.QQ ( sitemap )
55−import Servant.Utils.Links ( mkLink )
55+import Servant.Utils.Links ( safeLink, URI(..), IsElem, IsElem', HasLink(..) )
src/Servant/Utils/Links.hs+284−77
@@ -1,4 +1,5 @@
1{-# LANGUAGE PolyKinds #-}
1{-# LANGUAGE PolyKinds #-}
2+{-# LANGUAGE ConstraintKinds #-}
2{-# LANGUAGE DataKinds #-}
3{-# LANGUAGE DataKinds #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE TypeFamilies #-}
5{-# LANGUAGE TypeFamilies #-}
@@ -6,77 +7,146 @@
6{-# LANGUAGE FunctionalDependencies #-}
7{-# LANGUAGE FunctionalDependencies #-}
7{-# LANGUAGE ScopedTypeVariables #-}
8{-# LANGUAGE ScopedTypeVariables #-}
8{-# LANGUAGE UndecidableInstances #-}
9{-# LANGUAGE UndecidableInstances #-}
10+
9−-- | Type safe internal links.
11+-- | Type safe generation of internal links.
12+--
13+-- Given an API with a few endpoints:
14+--
15+-- >>> :set -XDataKinds -XTypeFamilies -XTypeOperators
16+-- >>> import Servant.API
17+-- >>> import Servant.Utils.Links
18+-- >>> import Data.Proxy
19+-- >>>
20+-- >>>
21+-- >>>
22+-- >>> type Hello = "hello" :> Get Int
23+-- >>> type Bye = "bye" :> QueryParam "name" String :> Delete
24+-- >>> type API = Hello :<|> Bye
25+-- >>> let api = Proxy :: Proxy API
26+--
27+-- It is possible to generate links that are guaranteed to be within 'API' with
28+-- 'safeLink'. The first argument to 'safeLink' is a type representing the API
29+-- you would like to restrict links to. The second argument is the destination
30+-- endpoint you would like the link to point to, this will need to end with a
31+-- verb like GET or POST. Further arguments may be required depending on the
32+-- type of the endpoint. If everything lines up you will get a 'URI' out the
33+-- other end.
10--
34--
11−-- Provides the function 'mkLink':
35+-- You may omit 'QueryParam's and the like should you not want to provide them,
36+-- but types which form part of the URL path like 'Capture' must be included.
37+-- The reason you may want to omit 'QueryParam's is that safeLink is a bit
38+-- magical: if parameters are included that could take input it will return a
39+-- function that accepts that input and generates a link. This is best shown
40+-- with an example. Here, a link is generated with no parameters:
12--
41--
13−-- @
14−-- type API = Proxy ("hello" :> Get Int
42+-- >>> let hello = Proxy :: Proxy ("hello" :> Get Int)
15−-- :<|> "bye" :> QueryParam "name" String :> Post Bool)
43+-- >>> print (safeLink api hello :: URI)
44+-- hello
16--
45--
17−-- api :: API
46+-- If the API has an endpoint with parameters then we can generate links with
18−-- api = proxy
47+-- or without those:
19--
48--
20−-- link1 :: Proxy ("hello" :> Get Int)
49+-- >>> let with = Proxy :: Proxy ("bye" :> QueryParam "name" String :> Delete)
21−-- link1 = proxy
50+-- >>> print $ safeLink api with "Hubert"
51+-- bye?name=Hubert
22--
52--
23−-- link2 :: Proxy ("hello" :> Delete)
53+-- >>> let without = Proxy :: Proxy ("bye" :> Delete)
24−-- link2 = proxy
54+-- >>> print $ safeLink api without
55+-- bye
25--
56--
26−-- mkLink link1 API -- typechecks, returns 'Link "/hello"'
57+-- If you would like create a helper for generating links only within that API,
58+-- you can partially apply safeLink if you specify a correct type signature
59+-- like so:
27--
60--
28−-- mkLink link2 API -- doesn't typecheck
61+-- >>> :set -XConstraintKinds
62+-- >>> :{
63+-- >>> let apiLink :: (IsElem endpoint API, HasLink endpoint)
64+-- >>> => Proxy endpoint -> MkLink endpoint
65+-- >>> apiLink = safeLink api
29−-- @
66+-- >>> :}
30--
67--
31−-- That is, 'mkLink' takes two arguments, a link proxy and a sitemap, and
68+-- Attempting to construct a link to an endpoint that does not exist in api
32−-- returns a 'Link', but only typechecks if the link proxy is a valid link,
69+-- will result in a type error like this:
33−-- and part of the sitemap.
34--
70--
35−-- __N.B.:__ 'mkLink' assumes a capture matches any string (without slashes).
71+-- >>> let bad_link = Proxy :: Proxy ("hello" :> Delete)
72+-- >>> safeLink api bad_link
73+-- <BLANKLINE>
74+-- <interactive>:64:1:
75+-- Could not deduce (Or
76+-- (IsElem' Delete (Get Int))
77+-- (IsElem'
78+-- ("hello" :> Delete)
79+-- ("bye" :> (QueryParam "name" String :> Delete))))
80+-- arising from a use of ‘safeLink’
81+-- In the expression: safeLink api bad_link
82+-- In an equation for ‘it’: it = safeLink api bad_link
83+--
84+-- This error is essentially saying that the type family couldn't find
85+-- bad_link under api after trying the open (but empty) type family
86+-- `IsElem'` as a last resort.
36module Servant.Utils.Links (
87module Servant.Utils.Links (
37− -- * Link and mkLink
88+ -- * Building and using safe links
38− -- | The only end-user utilities
89+ --
39− mkLink
90+ -- | Note that 'URI' is Network.URI.URI from the network-uri package.
40− , Link
91+ safeLink
41− -- * Internal
92+ , URI(..)
42− -- | These functions will likely only be of interest if you are writing
93+ -- * Adding custom types
43− -- more API combinators and would like to extend the behavior of
94+ , HasLink(..)
44− -- 'mkLink'
95+ , linkURI
45− , ValidLinkIn()
96+ , Link
46− , VLinkHelper(..)
97+ , IsElem'
98+ -- * Illustrative exports
47− , IsElem
99+ , IsElem
48− , IsLink
100+ , Or
49− )where
101+) where
50
102
103+import Data.List
51import Data.Proxy ( Proxy(..) )
104import Data.Proxy ( Proxy(..) )
105+import Data.Text (Text, unpack)
106+import Data.Monoid ( Monoid(..), (<>) )
107+import Network.URI ( URI(..), escapeURIString, isUnreserved )
52−import GHC.TypeLits ( KnownSymbol, Symbol, symbolVal )
108+import GHC.TypeLits ( KnownSymbol, symbolVal )
109+import GHC.Exts(Constraint)
53
110
111+import Servant.Common.Text
54import Servant.API.Capture ( Capture )
112import Servant.API.Capture ( Capture )
55import Servant.API.ReqBody ( ReqBody )
113import Servant.API.ReqBody ( ReqBody )
56import Servant.API.QueryParam ( QueryParam, QueryParams, QueryFlag )
114import Servant.API.QueryParam ( QueryParam, QueryParams, QueryFlag )
57import Servant.API.MatrixParam ( MatrixParam, MatrixParams, MatrixFlag )
115import Servant.API.MatrixParam ( MatrixParam, MatrixParams, MatrixFlag )
116+import Servant.API.Header ( Header )
58import Servant.API.Get ( Get )
117import Servant.API.Get ( Get )
59import Servant.API.Post ( Post )
118import Servant.API.Post ( Post )
60import Servant.API.Put ( Put )
119import Servant.API.Put ( Put )
61import Servant.API.Delete ( Delete )
120import Servant.API.Delete ( Delete )
62import Servant.API.Sub ( type (:>) )
121import Servant.API.Sub ( type (:>) )
122+import Servant.API.Raw ( Raw )
63import Servant.API.Alternative ( type (:<|>) )
123import Servant.API.Alternative ( type (:<|>) )
64
124
125+-- | If either a or b produce an empty constraint, produce an empty constraint.
126+type family Or (a :: Constraint) (b :: Constraint) :: Constraint where
127+ Or () b = ()
128+ Or a () = ()
65
129
66−type family Or a b where
130+-- | You may use this type family to tell the type checker that your custom type
67− Or 'False 'False = 'False
131+-- may be skipped as part of a link. This is useful for things like
68− Or 'True b = 'True
132+-- 'QueryParam' that are optional in a URI and do not affect them if they are
69− Or a 'True = 'True
133+-- omitted.
70−
134+--
71−type family And a b where
135+-- >>> data CustomThing
72− And 'True 'True = 'True
136+-- >>> type instance IsElem' e (CustomThing :> s) = IsElem e s
73− And a 'False = 'False
137+--
74− And 'False b = 'False
138+-- Note that 'IsElem' is called, which will mutually recurse back to `IsElem'`
139+-- if it exhausts all other options again.
140+--
141+-- Once you have written a HasLink instance for CustomThing you are ready to
142+-- go.
143+type family IsElem' a s :: Constraint
75
144
145+-- | Closed type family, check if endpoint is within api
76−type family IsElem a s where
146+type family IsElem endpoint api :: Constraint where
77 IsElem e (sa :<|> sb) = Or (IsElem e sa) (IsElem e sb)
147 IsElem e (sa :<|> sb) = Or (IsElem e sa) (IsElem e sb)
78 IsElem (e :> sa) (e :> sb) = IsElem sa sb
148 IsElem (e :> sa) (e :> sb) = IsElem sa sb
79− IsElem (e :> sa) (Capture x y :> sb) = IsElem sa sb
149+ IsElem sa (Header x :> sb) = IsElem sa sb
80 IsElem sa (ReqBody x :> sb) = IsElem sa sb
150 IsElem sa (ReqBody x :> sb) = IsElem sa sb
81 IsElem sa (QueryParam x y :> sb) = IsElem sa sb
151 IsElem sa (QueryParam x y :> sb) = IsElem sa sb
82 IsElem sa (QueryParams x y :> sb) = IsElem sa sb
152 IsElem sa (QueryParams x y :> sb) = IsElem sa sb
@@ -84,48 +154,185 @@ type family IsElem a s where
84 IsElem sa (MatrixParam x y :> sb) = IsElem sa sb
154 IsElem sa (MatrixParam x y :> sb) = IsElem sa sb
85 IsElem sa (MatrixParams x y :> sb) = IsElem sa sb
155 IsElem sa (MatrixParams x y :> sb) = IsElem sa sb
86 IsElem sa (MatrixFlag x :> sb) = IsElem sa sb
156 IsElem sa (MatrixFlag x :> sb) = IsElem sa sb
87− IsElem e e = 'True
157+ IsElem e e = ()
88− IsElem e a = 'False
158+ IsElem e a = IsElem' e a
89
159
90−type family IsLink'' l where
160+-- | A safe link datatype.
91− IsLink'' (e :> Get x) = IsLink' e
161+-- The only way of constructing a 'Link' is using 'safeLink', which means any
92− IsLink'' (e :> Post x) = IsLink' e
162+-- 'Link' is guaranteed to be part of the mentioned API.
93− IsLink'' (e :> Put x) = IsLink' e
163+data Link = Link
94− IsLink'' (e :> Delete) = IsLink' e
164+ { _segments :: [String] -- ^ Segments of "foo/bar" would be ["foo", "bar"]
95− IsLink'' a = 'False
165+ , _queryParams :: [Param Query]
166+ } deriving Show
96
167
97−type family IsLink' e where
98− IsLink' (f :: Symbol) = 'True
99
168
100−type family IsLink e where
169+-- Phantom types for Param
101− IsLink (a :> b) = Or (And (IsLink' a) (IsLink'' b))
170+data Matrix
102− (IsLink'' (a :> b))
171+data Query
103
172
173+-- | Query/Matrix param
174+data Param a
175+ = SingleParam String Text
176+ | ArrayElemParam String Text
177+ | FlagParam String
178+ deriving Show
104
179
105−-- | The 'ValidLinkIn f s' constraint holds when 's' is an API that
180+addSegment :: String -> Link -> Link
106−-- contains 'f', and 'f' is a link.
181+addSegment seg l = l { _segments = _segments l <> [seg] }
107−class ValidLinkIn f s where
108− mkLink :: f -> s -> Link -- ^ This function will only typecheck if `f`
109− -- is an URI within `s`
110
182
111−instance ( IsElem f s ~ 'True
183+addQueryParam :: Param Query -> Link -> Link
112− , IsLink f ~ 'True
184+addQueryParam qp l =
113− , VLinkHelper f) => ValidLinkIn f s where
185+ l { _queryParams = _queryParams l <> [qp] }
114− mkLink _ _ = Link (vlh (Proxy :: Proxy f))
115
186
116−-- | A safe link datatype.
187+-- Not particularly efficient for many updates. Something to optimise if it's
117−-- The only way of constructing a 'Link' is using 'mkLink', which means any
188+-- a problem.
118−-- 'Link' is guaranteed to be part of the mentioned API.
189+addMatrixParam :: Param Matrix -> Link -> Link
119−data Link = Link String deriving Show
190+addMatrixParam param l = l { _segments = f (_segments l) }
191+ where
192+ f [] = []
193+ f xs = init xs <> [g (last xs)]
194+ -- Modify the segment at the "top" of the stack
195+ g :: String -> String
196+ g seg =
197+ case param of
198+ SingleParam k v -> seg <> ";" <> k <> "=" <> escape (unpack v)
199+ ArrayElemParam k v -> seg <> ";" <> k <> "[]=" <> escape (unpack v)
200+ FlagParam k -> seg <> ";" <> k
201+
202+linkURI :: Link -> URI
203+linkURI (Link segments q_params) =
204+ URI mempty -- No scheme (relative)
205+ Nothing -- Or authority (relative)
206+ (intercalate "/" segments)
207+ (makeQueries q_params) mempty
208+ where
209+ makeQueries :: [Param Query] -> String
210+ makeQueries [] = ""
211+ makeQueries xs =
212+ "?" <> intercalate "&" (fmap makeQuery xs)
213+
214+ makeQuery :: Param Query -> String
215+ makeQuery (ArrayElemParam k v) = escape k <> "[]=" <> escape (unpack v)
216+ makeQuery (SingleParam k v) = escape k <> "=" <> escape (unpack v)
217+ makeQuery (FlagParam k) = escape k
218+
219+escape :: String -> String
220+escape = escapeURIString isUnreserved
221+
222+-- | Create a valid (by construction) relative URI with query params.
223+--
224+-- This function will only typecheck if `endpoint` is part of the API `api`
225+safeLink
226+ :: forall endpoint api. (IsElem endpoint api, HasLink endpoint)
227+ => Proxy api -- ^ The whole API that this endpoint is a part of
228+ -> Proxy endpoint -- ^ The API endpoint you would like to point to
229+ -> MkLink endpoint
230+safeLink _ endpoint = toLink endpoint (Link mempty mempty)
231+
232+-- | Construct a toLink for an endpoint.
233+class HasLink endpoint where
234+ type MkLink endpoint
235+ toLink :: Proxy endpoint -- ^ The API endpoint you would like to point to
236+ -> Link
237+ -> MkLink endpoint
238+
239+-- Naked symbol instance
240+instance (KnownSymbol sym, HasLink sub) => HasLink (sym :> sub) where
241+ type MkLink (sym :> sub) = MkLink sub
242+ toLink _ =
243+ toLink (Proxy :: Proxy sub) . addSegment seg
244+ where
245+ seg = symbolVal (Proxy :: Proxy sym)
246+
247+
248+-- QueryParam instances
249+instance (KnownSymbol sym, ToText v, HasLink sub)
250+ => HasLink (QueryParam sym v :> sub) where
251+ type MkLink (QueryParam sym v :> sub) = v -> MkLink sub
252+ toLink _ l v =
253+ toLink (Proxy :: Proxy sub)
254+ (addQueryParam (SingleParam k (toText v)) l)
255+ where
256+ k :: String
257+ k = symbolVal (Proxy :: Proxy sym)
258+
259+instance (KnownSymbol sym, ToText v, HasLink sub)
260+ => HasLink (QueryParams sym v :> sub) where
261+ type MkLink (QueryParams sym v :> sub) = [v] -> MkLink sub
262+ toLink _ l =
263+ toLink (Proxy :: Proxy sub) .
264+ foldl' (\l' v -> addQueryParam (ArrayElemParam k (toText v)) l') l
265+ where
266+ k = symbolVal (Proxy :: Proxy sym)
267+
268+instance (KnownSymbol sym, HasLink sub)
269+ => HasLink (QueryFlag sym :> sub) where
270+ type MkLink (QueryFlag sym :> sub) = Bool -> MkLink sub
271+ toLink _ l False =
272+ toLink (Proxy :: Proxy sub) l
273+ toLink _ l True =
274+ toLink (Proxy :: Proxy sub) $ addQueryParam (FlagParam k) l
275+ where
276+ k = symbolVal (Proxy :: Proxy sym)
277+
278+-- MatrixParam instances
279+instance (KnownSymbol sym, ToText v, HasLink sub)
280+ => HasLink (MatrixParam sym v :> sub) where
281+ type MkLink (MatrixParam sym v :> sub) = v -> MkLink sub
282+ toLink _ l v =
283+ toLink (Proxy :: Proxy sub) $
284+ addMatrixParam (SingleParam k (toText v)) l
285+ where
286+ k = symbolVal (Proxy :: Proxy sym)
287+
288+instance (KnownSymbol sym, ToText v, HasLink sub)
289+ => HasLink (MatrixParams sym v :> sub) where
290+ type MkLink (MatrixParams sym v :> sub) = [v] -> MkLink sub
291+ toLink _ l =
292+ toLink (Proxy :: Proxy sub) .
293+ foldl' (\l' v -> addMatrixParam (ArrayElemParam k (toText v)) l') l
294+ where
295+ k = symbolVal (Proxy :: Proxy sym)
296+
297+instance (KnownSymbol sym, HasLink sub)
298+ => HasLink (MatrixFlag sym :> sub) where
299+ type MkLink (MatrixFlag sym :> sub) = Bool -> MkLink sub
300+ toLink _ l False =
301+ toLink (Proxy :: Proxy sub) l
302+ toLink _ l True =
303+ toLink (Proxy :: Proxy sub) $ addMatrixParam (FlagParam k) l
304+ where
305+ k = symbolVal (Proxy :: Proxy sym)
306+
307+-- Misc instances
308+instance HasLink sub => HasLink (ReqBody a :> sub) where
309+ type MkLink (ReqBody a :> sub) = MkLink sub
310+ toLink _ = toLink (Proxy :: Proxy sub)
311+
312+instance (ToText v, HasLink sub)
313+ => HasLink (Capture sym v :> sub) where
314+ type MkLink (Capture sym v :> sub) = v -> MkLink sub
315+ toLink _ l v =
316+ toLink (Proxy :: Proxy sub) $
317+ addSegment (escape . unpack $ toText v) l
318+
319+-- Verb (terminal) instances
320+instance HasLink (Get r) where
321+ type MkLink (Get r) = URI
322+ toLink _ = linkURI
120
323
121−class VLinkHelper f where
324+instance HasLink (Post r) where
122− vlh :: forall proxy. proxy f -> String
325+ type MkLink (Post r) = URI
326+ toLink _ = linkURI
123
327
124−instance (KnownSymbol s, VLinkHelper e) => VLinkHelper (s :> e) where
328+instance HasLink (Put r) where
125− vlh _ = "/" ++ symbolVal (Proxy :: Proxy s) ++ vlh (Proxy :: Proxy e)
329+ type MkLink (Put r) = URI
330+ toLink _ = linkURI
126
331
127−instance VLinkHelper (Get x) where
332+instance HasLink Delete where
128− vlh _ = ""
333+ type MkLink Delete = URI
334+ toLink _ = linkURI
129
335
130−instance VLinkHelper (Post x) where
336+instance HasLink Raw where
131− vlh _ = ""
337+ type MkLink Raw = URI
338+ toLink _ = linkURI
test-docs.shnew file+3
@@ -0,0 +1,3 @@
1+#!/bin/sh
2+
3+doctest -isrc -optP-include -optPdist/build/autogen/cabal_macros.h $(find src/ -name '*.hs')
test/Servant/Utils/LinksSpec.hs+63−42
@@ -1,59 +1,80 @@
1{-# LANGUAGE DataKinds #-}
1{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE PolyKinds #-}
2{-# LANGUAGE PolyKinds #-}
3{-# LANGUAGE TypeOperators #-}
3{-# LANGUAGE TypeOperators #-}
4+{-# LANGUAGE ConstraintKinds #-}
5+
4module Servant.Utils.LinksSpec where
6module Servant.Utils.LinksSpec where
5
7
6−import Test.Hspec ( Spec, it, describe )
8+import Test.Hspec ( Spec, it, describe, shouldBe, Expectation )
9+import Data.Proxy ( Proxy(..) )
7
10
8import Servant.API
11import Servant.API
9− ( type (:<|>), ReqBody, QueryParam, MatrixParam, MatrixParams, MatrixFlag, Get, Post, Capture, type (:>) )
10−import Servant.QQSpec ( (~>) )
11−import Servant.Utils.Links ( IsElem, IsLink )
12−
13
12
14type TestApi =
13type TestApi =
14+ -- Capture and query/matrix params
15− "hello" :> Capture "name" String :> QueryParam "capital" Bool :> Get Bool
15+ "hello" :> Capture "name" String :> QueryParam "capital" Bool :> Delete
16− :<|> "greet" :> ReqBody 'True :> Post Bool
16+
17− :<|> "parent" :> MatrixParams "name" String :> "child" :> MatrixParam "gender" String :> Get String
17+ :<|> "parent" :> MatrixParams "name" String :> "child"
18+ :> MatrixParam "gender" String :> Get String
19+
20+ -- Flags
21+ :<|> "ducks" :> MatrixFlag "yellow" :> MatrixFlag "loud" :> Delete
22+ :<|> "balls" :> QueryFlag "bouncy" :> QueryFlag "fast" :> Delete
23+
24+ -- All of the verbs
25+ :<|> "get" :> Get ()
26+ :<|> "put" :> Put ()
27+ :<|> "post" :> ReqBody 'True :> Post ()
28+ :<|> "delete" :> Header "ponies" :> Delete
29+ :<|> "raw" :> Raw
18
30
19type TestLink = "hello" :> "hi" :> Get Bool
31type TestLink = "hello" :> "hi" :> Get Bool
20type TestLink2 = "greet" :> Post Bool
32type TestLink2 = "greet" :> Post Bool
21type TestLink3 = "parent" :> "child" :> Get String
33type TestLink3 = "parent" :> "child" :> Get String
22
34
23−type BadTestLink = "hallo" :> "hi" :> Get Bool
35+apiLink :: (IsElem endpoint TestApi, HasLink endpoint)
24−type BadTestLink2 = "greet" :> Get Bool
36+ => Proxy endpoint -> MkLink endpoint
25−type BadTestLink3 = "parent" :> "child" :> MatrixFlag "male" :> Get String
37+apiLink = safeLink (Proxy :: Proxy TestApi)
26−
27−type NotALink = "hello" :> Capture "x" Bool :> Get Bool
28−type NotALink2 = "hello" :> ReqBody 'True :> Get Bool
29
38
30−data Proxy x = Proxy
39+-- | Convert a link to a URI and ensure that this maps to the given string
31−class ReflectT (x::Bool) where { reflected :: Proxy x -> Bool }
40+-- given string
32−instance ReflectT 'True where { reflected _ = True }
41+shouldBeURI :: URI -> String -> Expectation
33−instance ReflectT 'False where { reflected _ = False }
42+shouldBeURI link expected =
43+ show link `shouldBe` expected
34
44
35spec :: Spec
45spec :: Spec
36−spec = describe "Servant.API.Elem" $ do
46+spec = describe "Servant.Utils.Links" $ do
37− isElem
47+ it "Generates correct links for capture query and matrix params" $ do
38− isLink
48+ let l1 = Proxy :: Proxy ("hello" :> Capture "name" String :> Delete)
39−
49+ apiLink l1 "hi" `shouldBeURI` "hello/hi"
40−isElem :: Spec
50+
41−isElem = describe "IsElem" $ do
51+ let l2 = Proxy :: Proxy ("hello" :> Capture "name" String
42− it "is True when the first argument is an url within the second" $ do
52+ :> QueryParam "capital" Bool
43− reflected (Proxy::Proxy (IsElem TestLink TestApi)) ~> True
53+ :> Delete)
44− reflected (Proxy::Proxy (IsElem TestLink2 TestApi)) ~> True
54+ apiLink l2 "bye" True `shouldBeURI` "hello/bye?capital=true"
45− reflected (Proxy::Proxy (IsElem TestLink3 TestApi)) ~> True
55+
46− it "is False when the first argument is not an url within the second" $ do
56+ let l3 = Proxy :: Proxy ("parent" :> MatrixParams "name" String
47− reflected (Proxy::Proxy (IsElem BadTestLink TestApi)) ~> False
57+ :> "child"
48− reflected (Proxy::Proxy (IsElem BadTestLink2 TestApi)) ~> False
58+ :> MatrixParam "gender" String
49− reflected (Proxy::Proxy (IsElem BadTestLink3 TestApi)) ~> False
59+ :> Get String)
50−
60+ apiLink l3 ["Hubert?x=;&", "Cumberdale"] "Edward?"
51−isLink :: Spec
61+ `shouldBeURI` "parent;name[]=Hubert%3Fx%3D%3B%26;\
52−isLink = describe "IsLink" $ do
62+ \name[]=Cumberdale/child;gender=Edward%3F"
53− it "is True when all Subs are paths and the last is a method" $ do
63+
54− reflected (Proxy::Proxy (IsLink TestLink)) ~> True
64+ it "Generates correct links for query and matrix flags" $ do
55− reflected (Proxy::Proxy (IsLink TestLink2)) ~> True
65+ let l1 = Proxy :: Proxy ("balls" :> QueryFlag "bouncy"
56− reflected (Proxy::Proxy (IsLink TestLink3)) ~> True
66+ :> QueryFlag "fast" :> Delete)
57− it "is False of anything with captures" $ do
67+ apiLink l1 True True `shouldBeURI` "balls?bouncy&fast"
58− reflected (Proxy::Proxy (IsLink NotALink)) ~> False
68+ apiLink l1 False True `shouldBeURI` "balls?fast"
59− reflected (Proxy::Proxy (IsLink NotALink2)) ~> False
69+
70+ let l2 = Proxy :: Proxy ("ducks" :> MatrixFlag "yellow"
71+ :> MatrixFlag "loud" :> Delete)
72+ apiLink l2 True True `shouldBeURI` "ducks;yellow;loud"
73+ apiLink l2 False True `shouldBeURI` "ducks;loud"
74+
75+ it "Generates correct links for all of the verbs" $ do
76+ apiLink (Proxy :: Proxy ("get" :> Get ())) `shouldBeURI` "get"
77+ apiLink (Proxy :: Proxy ("put" :> Put ())) `shouldBeURI` "put"
78+ apiLink (Proxy :: Proxy ("post" :> Post ())) `shouldBeURI` "post"
79+ apiLink (Proxy :: Proxy ("delete" :> Delete)) `shouldBeURI` "delete"
80+ apiLink (Proxy :: Proxy ("raw" :> Raw)) `shouldBeURI` "raw"