Skip to content

Commit 809ca37

Browse files
authored
Merge pull request #1699 from RaoulHC/NamedRoutes-IsElem
Add missing IsElem instance for NamedRoutes
2 parents 72f5d5c + 12033e7 commit 809ca37

File tree

4 files changed

+81
-1
lines changed

4 files changed

+81
-1
lines changed

cabal.project

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ optimization: False
5858
constraints: crypton < 0, crypton-connection < 0, crypton-x509 < 0, crypton-x509-store < 0, crypton-x509-system < 0, crypton-x509-validation < 0
5959
constraints: warp < 3.3.26
6060

61+
-- wreq-0.5.4.1 doesn't seem to work with ghc-8.6.5
62+
if (impl(ghc < 8.8))
63+
constraints: wreq == 0.5.4.0
64+
6165
allow-newer: servant-js:base
6266

6367
-- Print ticks so that doctest type querying is consistent across GHC versions.

changelog.d/1699

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
synopsis: Add NamedRoutes instance to IsElem
2+
prs: #1699
3+
issues: #1674
4+
description: {
5+
Add missing IsElem instance for NamedRoutes, this allows links to be checked
6+
with `safeLink`.
7+
}

servant/src/Servant/API/TypeLevel.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ import Servant.API.QueryParam
6464
(QueryFlag, QueryParam, QueryParams)
6565
import Servant.API.ReqBody
6666
(ReqBody)
67+
import Servant.API.NamedRoutes
68+
(NamedRoutes)
69+
import Servant.API.Generic
70+
(ToServantApi)
6771
import Servant.API.Sub
6872
(type (:>))
6973
import Servant.API.Verbs
@@ -143,6 +147,7 @@ type family IsElem endpoint api :: Constraint where
143147
IsElem (Verb m s ct typ) (Verb m s ct' typ)
144148
= IsSubList ct ct'
145149
IsElem e e = ()
150+
IsElem e (NamedRoutes rs) = IsElem e (ToServantApi rs)
146151
IsElem e a = IsElem' e a
147152

148153
-- | Check whether @sub@ is a sub-API of @api@.

servant/test/Servant/LinksSpec.hs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
{-# LANGUAGE ConstraintKinds #-}
22
{-# LANGUAGE DataKinds #-}
3+
{-# LANGUAGE DeriveGeneric #-}
34
{-# LANGUAGE PolyKinds #-}
45
{-# LANGUAGE ScopedTypeVariables #-}
56
{-# LANGUAGE TypeOperators #-}
67
module Servant.LinksSpec where
78

9+
import GHC.Generics
10+
(Generic)
811
import Data.Proxy
912
(Proxy (..))
1013
import Data.String
@@ -44,17 +47,51 @@ type LinkableApi =
4447
"all" :> CaptureAll "names" String :> Get '[JSON] NoContent
4548
:<|> "get" :> Get '[JSON] NoContent
4649

47-
4850
apiLink :: (IsElem endpoint TestApi, HasLink endpoint)
4951
=> Proxy endpoint -> MkLink endpoint Link
5052
apiLink = safeLink (Proxy :: Proxy TestApi)
5153

54+
55+
newtype QuuxRoutes mode = QuuxRoutes
56+
{ corge :: mode :- "corge" :> Post '[PlainText] NoContent
57+
} deriving Generic
58+
59+
newtype WaldoRoutes mode = WaldoRoutes
60+
{ waldo :: mode :- "waldo" :> Get '[JSON] NoContent
61+
} deriving Generic
62+
63+
data FooRoutes mode = FooRoutes
64+
{ baz :: mode :- "baz" :> Get '[JSON] NoContent
65+
, qux :: mode :- "qux" :> NamedRoutes QuuxRoutes
66+
, quux :: mode :- "quux" :> QueryParam "grault" String :> Get '[JSON] NoContent
67+
, garply :: mode :- "garply" :> Capture "garply" String
68+
:> Capture "garplyNum" Int :> NamedRoutes WaldoRoutes
69+
} deriving Generic
70+
71+
data BaseRoutes mode = BaseRoutes
72+
{ foo :: mode :- "foo" :> NamedRoutes FooRoutes
73+
, bar :: mode :- "bar" :> Get '[JSON] NoContent
74+
} deriving Generic
75+
76+
recordApiLink
77+
:: (IsElem endpoint (NamedRoutes BaseRoutes), HasLink endpoint)
78+
=> Proxy endpoint -> MkLink endpoint Link
79+
recordApiLink = safeLink (Proxy :: Proxy (NamedRoutes BaseRoutes))
80+
5281
-- | Convert a link to a URI and ensure that this maps to the given string
5382
-- given string
5483
shouldBeLink :: Link -> String -> Expectation
5584
shouldBeLink link expected =
5685
toUrlPiece link `shouldBe` fromString expected
5786

87+
(//) :: a -> (a -> b) -> b
88+
x // f = f x
89+
infixl 1 //
90+
91+
(/:) :: (a -> b -> c) -> b -> a -> c
92+
(/:) = flip
93+
infixl 2 /:
94+
5895
spec :: Spec
5996
spec = describe "Servant.Links" $ do
6097
it "generates correct links for capture query params" $ do
@@ -106,6 +143,33 @@ spec = describe "Servant.Links" $ do
106143
let firstLink :<|> _ = allLinks comprehensiveAPIWithoutRaw
107144
firstLink `shouldBeLink` ""
108145

146+
it "Generate links from record fields accessors" $ do
147+
fieldLink bar `shouldBeLink` "bar"
148+
(fieldLink foo // baz) `shouldBeLink` "foo/baz"
149+
(fieldLink foo // qux // corge) `shouldBeLink` "foo/qux/corge"
150+
(fieldLink foo // quux /: Nothing) `shouldBeLink` "foo/quux"
151+
(fieldLink foo // quux /: Just "floop") `shouldBeLink` "foo/quux?grault=floop"
152+
(fieldLink foo // garply /: "captureme" /: 42 // waldo)
153+
`shouldBeLink` "foo/garply/captureme/42/waldo"
154+
155+
it "Check links from record fields" $ do
156+
let sub1 = Proxy :: Proxy ("bar" :> Get '[JSON] NoContent)
157+
recordApiLink sub1 `shouldBeLink` "bar"
158+
159+
let sub2 = Proxy :: Proxy ("foo" :> "baz" :> Get '[JSON] NoContent)
160+
recordApiLink sub2 `shouldBeLink` "foo/baz"
161+
162+
let sub3 = Proxy :: Proxy ("foo" :> "quux" :> QueryParam "grault" String
163+
:> Get '[JSON] NoContent)
164+
recordApiLink sub3 (Just "floop") `shouldBeLink` "foo/quux?grault=floop"
165+
166+
let sub4 :: Proxy ("foo" :> "garply" :> Capture "garplyText" String
167+
:> Capture "garplyInt" Int :> "waldo"
168+
:> Get '[JSON] NoContent)
169+
sub4 = Proxy
170+
recordApiLink sub4 "captureme" 42
171+
`shouldBeLink` "foo/garply/captureme/42/waldo"
172+
109173
-- The doctests below aren't run on CI, setting that up is tricky.
110174
-- They are run by makefile rule, however.
111175

0 commit comments

Comments
 (0)