Skip to content

Commit 3c52f8c

Browse files
committed
Display capture hints in router layout
This commit introduces a `CaptureHint` type, which is passed as an extra argument to the `CaptureRouter` and `CaptureAllRouter` constructors for the `Router'` type. `CaptureHint` values are then used in `routerLayout`, to display the name and type of captured values, instead of just "<capture>" previously. N.B.: Because the `choice` smart constructor for routers can aggregate `Capture` combinators with different capture hints, the `Capture*Router` constructors actually take a *list* of `CaptureHint`, instead of a single one.
1 parent de923fc commit 3c52f8c

File tree

4 files changed

+57
-22
lines changed

4 files changed

+57
-22
lines changed

servant-server/src/Servant/Server.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ hoistServer p = hoistServerWithContext p (Proxy :: Proxy '[])
235235
-- > │ └─ e/
236236
-- > │ └─•
237237
-- > ├─ b/
238-
-- > │ └─ <capture>/
238+
-- > │ └─ <x::Int>/
239239
-- > │ ├─•
240240
-- > │ ┆
241241
-- > │ └─•
@@ -252,7 +252,8 @@ hoistServer p = hoistServerWithContext p (Proxy :: Proxy '[])
252252
--
253253
-- [@─•@] Leaves reflect endpoints.
254254
--
255-
-- [@\<capture\>/@] This is a delayed capture of a path component.
255+
-- [@\<x::Int\>/@] This is a delayed capture of a path component
256+
-- named @x@ with type @Int@.
256257
--
257258
-- [@\<raw\>@] This is a part of the API we do not know anything about.
258259
--

servant-server/src/Servant/Server/Internal.hs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ instance (HasServer a context, HasServer b context) => HasServer (a :<|> b) cont
173173
-- > server = getBook
174174
-- > where getBook :: Text -> Handler Book
175175
-- > getBook isbn = ...
176-
instance (KnownSymbol capture, FromHttpApiData a
176+
instance (KnownSymbol capture, FromHttpApiData a, Typeable a
177177
, HasServer api context, SBoolI (FoldLenient mods)
178178
, HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters
179179
)
@@ -185,7 +185,7 @@ instance (KnownSymbol capture, FromHttpApiData a
185185
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
186186

187187
route Proxy context d =
188-
CaptureRouter $
188+
CaptureRouter [hint] $
189189
route (Proxy :: Proxy api)
190190
context
191191
(addCapture d $ \ txt -> withRequest $ \ request ->
@@ -197,6 +197,7 @@ instance (KnownSymbol capture, FromHttpApiData a
197197
where
198198
rep = typeRep (Proxy :: Proxy Capture')
199199
formatError = urlParseErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context)
200+
hint = CaptureHint (T.pack $ symbolVal $ Proxy @capture) (typeRep $ Proxy @a)
200201

201202
-- | If you use 'CaptureAll' in one of the endpoints for your API,
202203
-- this automatically requires your server-side handler to be a
@@ -215,7 +216,7 @@ instance (KnownSymbol capture, FromHttpApiData a
215216
-- > server = getSourceFile
216217
-- > where getSourceFile :: [Text] -> Handler Book
217218
-- > getSourceFile pathSegments = ...
218-
instance (KnownSymbol capture, FromHttpApiData a
219+
instance (KnownSymbol capture, FromHttpApiData a, Typeable a
219220
, HasServer api context
220221
, HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters
221222
)
@@ -227,7 +228,7 @@ instance (KnownSymbol capture, FromHttpApiData a
227228
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
228229

229230
route Proxy context d =
230-
CaptureAllRouter $
231+
CaptureAllRouter [hint] $
231232
route (Proxy :: Proxy api)
232233
context
233234
(addCapture d $ \ txts -> withRequest $ \ request ->
@@ -238,6 +239,7 @@ instance (KnownSymbol capture, FromHttpApiData a
238239
where
239240
rep = typeRep (Proxy :: Proxy CaptureAll)
240241
formatError = urlParseErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context)
242+
hint = CaptureHint (T.pack $ symbolVal $ Proxy @capture) (typeRep $ Proxy @[a])
241243

242244
allowedMethodHead :: Method -> Request -> Bool
243245
allowedMethodHead method request = method == methodGet && requestMethod request == methodHead

servant-server/src/Servant/Server/Internal/Router.hs

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@ import Prelude.Compat
99

1010
import Data.Function
1111
(on)
12+
import Data.List
13+
(nub)
1214
import Data.Map
1315
(Map)
1416
import qualified Data.Map as M
1517
import Data.Text
1618
(Text)
1719
import qualified Data.Text as T
20+
import Data.Typeable
21+
(TypeRep)
1822
import Network.Wai
1923
(Response, pathInfo)
2024
import Servant.Server.Internal.ErrorFormatter
@@ -24,6 +28,18 @@ import Servant.Server.Internal.ServerError
2428

2529
type Router env = Router' env RoutingApplication
2630

31+
data CaptureHint = CaptureHint
32+
{ captureName :: Text
33+
, capturedType :: TypeRep
34+
}
35+
deriving (Show, Eq)
36+
37+
toCaptureTag :: CaptureHint -> Text
38+
toCaptureTag hint = captureName hint <> "::" <> (T.pack . show) (capturedType hint)
39+
40+
toCaptureTags :: [CaptureHint] -> Text
41+
toCaptureTags hints = "<" <> T.intercalate "|" (map toCaptureTag hints) <> ">"
42+
2743
-- | Internal representation of a router.
2844
--
2945
-- The first argument describes an environment type that is
@@ -36,10 +52,10 @@ data Router' env a =
3652
-- ^ the map contains routers for subpaths (first path component used
3753
-- for lookup and removed afterwards), the list contains handlers
3854
-- for the empty path, to be tried in order
39-
| CaptureRouter (Router' (Text, env) a)
55+
| CaptureRouter [CaptureHint] (Router' (Text, env) a)
4056
-- ^ first path component is passed to the child router in its
4157
-- environment and removed afterwards
42-
| CaptureAllRouter (Router' ([Text], env) a)
58+
| CaptureAllRouter [CaptureHint] (Router' ([Text], env) a)
4359
-- ^ all path components are passed to the child router in its
4460
-- environment and are removed afterwards
4561
| RawRouter (env -> a)
@@ -69,8 +85,8 @@ leafRouter l = StaticRouter M.empty [l]
6985
choice :: Router' env a -> Router' env a -> Router' env a
7086
choice (StaticRouter table1 ls1) (StaticRouter table2 ls2) =
7187
StaticRouter (M.unionWith choice table1 table2) (ls1 ++ ls2)
72-
choice (CaptureRouter router1) (CaptureRouter router2) =
73-
CaptureRouter (choice router1 router2)
88+
choice (CaptureRouter hints1 router1) (CaptureRouter hints2 router2) =
89+
CaptureRouter (nub $ hints1 ++ hints2) (choice router1 router2)
7490
choice router1 (Choice router2 router3) = Choice (choice router1 router2) router3
7591
choice router1 router2 = Choice router1 router2
7692

@@ -84,7 +100,7 @@ choice router1 router2 = Choice router1 router2
84100
--
85101
data RouterStructure =
86102
StaticRouterStructure (Map Text RouterStructure) Int
87-
| CaptureRouterStructure RouterStructure
103+
| CaptureRouterStructure [CaptureHint] RouterStructure
88104
| RawRouterStructure
89105
| ChoiceStructure RouterStructure RouterStructure
90106
deriving (Eq, Show)
@@ -98,11 +114,11 @@ data RouterStructure =
98114
routerStructure :: Router' env a -> RouterStructure
99115
routerStructure (StaticRouter m ls) =
100116
StaticRouterStructure (fmap routerStructure m) (length ls)
101-
routerStructure (CaptureRouter router) =
102-
CaptureRouterStructure $
117+
routerStructure (CaptureRouter hints router) =
118+
CaptureRouterStructure hints $
103119
routerStructure router
104-
routerStructure (CaptureAllRouter router) =
105-
CaptureRouterStructure $
120+
routerStructure (CaptureAllRouter hints router) =
121+
CaptureRouterStructure hints $
106122
routerStructure router
107123
routerStructure (RawRouter _) =
108124
RawRouterStructure
@@ -111,11 +127,21 @@ routerStructure (Choice r1 r2) =
111127
(routerStructure r1)
112128
(routerStructure r2)
113129

114-
-- | Compare the structure of two routers.
130+
-- | Compare the structure of two routers. Ignores capture hints.
115131
--
116132
sameStructure :: Router' env a -> Router' env b -> Bool
117-
sameStructure r1 r2 =
118-
routerStructure r1 == routerStructure r2
133+
sameStructure router1 router2 =
134+
routerStructure router1 `almostEq` routerStructure router2
135+
where
136+
almostEq :: RouterStructure -> RouterStructure -> Bool
137+
almostEq (StaticRouterStructure m1 l1) (StaticRouterStructure m2 l2) =
138+
l1 == l2 && M.isSubmapOfBy almostEq m1 m2 && M.isSubmapOfBy almostEq m2 m1
139+
almostEq (CaptureRouterStructure _ r1) (CaptureRouterStructure _ r2) =
140+
r1 `almostEq` r2
141+
almostEq RawRouterStructure RawRouterStructure = True
142+
almostEq (ChoiceStructure r1 r1') (ChoiceStructure r2 r2') =
143+
r1 `almostEq` r2 && r1' `almostEq` r2'
144+
almostEq _ _ = False
119145

120146
-- | Provide a textual representation of the
121147
-- structure of a router.
@@ -126,7 +152,8 @@ routerLayout router =
126152
where
127153
mkRouterLayout :: Bool -> RouterStructure -> [Text]
128154
mkRouterLayout c (StaticRouterStructure m n) = mkSubTrees c (M.toList m) n
129-
mkRouterLayout c (CaptureRouterStructure r) = mkSubTree c "<capture>" (mkRouterLayout False r)
155+
mkRouterLayout c (CaptureRouterStructure hints r) =
156+
mkSubTree c (toCaptureTags hints) (mkRouterLayout False r)
130157
mkRouterLayout c RawRouterStructure =
131158
if c then ["├─ <raw>"] else ["└─ <raw>"]
132159
mkRouterLayout c (ChoiceStructure r1 r2) =
@@ -169,15 +196,15 @@ runRouterEnv fmt router env request respond =
169196
-> let request' = request { pathInfo = rest }
170197
in runRouterEnv fmt router' env request' respond
171198
_ -> respond $ Fail $ fmt request
172-
CaptureRouter router' ->
199+
CaptureRouter _ router' ->
173200
case pathInfo request of
174201
[] -> respond $ Fail $ fmt request
175202
-- This case is to handle trailing slashes.
176203
[""] -> respond $ Fail $ fmt request
177204
first : rest
178205
-> let request' = request { pathInfo = rest }
179206
in runRouterEnv fmt router' (first, env) request' respond
180-
CaptureAllRouter router' ->
207+
CaptureAllRouter _ router' ->
181208
let segments = pathInfo request
182209
request' = request { pathInfo = [] }
183210
in runRouterEnv fmt router' (segments, env) request' respond

servant-server/test/Servant/Server/RouterSpec.hs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import Data.Proxy
1010
(Proxy (..))
1111
import Data.Text
1212
(unpack)
13+
import Data.Typeable
14+
(typeRep)
1315
import Network.HTTP.Types
1416
(Status (..))
1517
import Network.Wai
@@ -51,14 +53,17 @@ routerSpec = do
5153
toApp = toApplication . runRouter (const err404)
5254

5355
cap :: Router ()
54-
cap = CaptureRouter $
56+
cap = CaptureRouter [hint] $
5557
let delayed = addCapture (emptyDelayed $ Route pure) (const $ delayedFail err400)
5658
in leafRouter
5759
$ \env req res ->
5860
runAction delayed env req res
5961
. const
6062
$ Route success
6163

64+
hint :: CaptureHint
65+
hint = CaptureHint "anything" $ typeRep (Proxy :: Proxy ())
66+
6267
router :: Router ()
6368
router = leafRouter (\_ _ res -> res $ Route success)
6469
`Choice` cap

0 commit comments

Comments
 (0)