Skip to content

Commit a8ffbf0

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 (single or list), 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 a8ffbf0

File tree

4 files changed

+55
-21
lines changed

4 files changed

+55
-21
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::CaptureSingle>/
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::CaptureSingle\>/@] This is a delayed capture of a single
256+
-- path component named @x@.
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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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) CaptureSingle
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
@@ -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) CaptureList
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: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ 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
@@ -24,6 +26,21 @@ import Servant.Server.Internal.ServerError
2426

2527
type Router env = Router' env RoutingApplication
2628

29+
data CaptureHint = CaptureHint
30+
{ captureName :: Text
31+
, captureType :: CaptureType
32+
}
33+
deriving (Show, Eq)
34+
35+
data CaptureType = CaptureList | CaptureSingle
36+
deriving (Show, Eq)
37+
38+
toCaptureTag :: CaptureHint -> Text
39+
toCaptureTag hint = captureName hint <> "::" <> (T.pack . show) (captureType hint)
40+
41+
toCaptureTags :: [CaptureHint] -> Text
42+
toCaptureTags hints = "<" <> T.intercalate "|" (map toCaptureTag hints) <> ">"
43+
2744
-- | Internal representation of a router.
2845
--
2946
-- The first argument describes an environment type that is
@@ -36,10 +53,10 @@ data Router' env a =
3653
-- ^ the map contains routers for subpaths (first path component used
3754
-- for lookup and removed afterwards), the list contains handlers
3855
-- for the empty path, to be tried in order
39-
| CaptureRouter (Router' (Text, env) a)
56+
| CaptureRouter [CaptureHint] (Router' (Text, env) a)
4057
-- ^ first path component is passed to the child router in its
4158
-- environment and removed afterwards
42-
| CaptureAllRouter (Router' ([Text], env) a)
59+
| CaptureAllRouter [CaptureHint] (Router' ([Text], env) a)
4360
-- ^ all path components are passed to the child router in its
4461
-- environment and are removed afterwards
4562
| RawRouter (env -> a)
@@ -69,8 +86,8 @@ leafRouter l = StaticRouter M.empty [l]
6986
choice :: Router' env a -> Router' env a -> Router' env a
7087
choice (StaticRouter table1 ls1) (StaticRouter table2 ls2) =
7188
StaticRouter (M.unionWith choice table1 table2) (ls1 ++ ls2)
72-
choice (CaptureRouter router1) (CaptureRouter router2) =
73-
CaptureRouter (choice router1 router2)
89+
choice (CaptureRouter hints1 router1) (CaptureRouter hints2 router2) =
90+
CaptureRouter (nub $ hints1 ++ hints2) (choice router1 router2)
7491
choice router1 (Choice router2 router3) = Choice (choice router1 router2) router3
7592
choice router1 router2 = Choice router1 router2
7693

@@ -84,7 +101,7 @@ choice router1 router2 = Choice router1 router2
84101
--
85102
data RouterStructure =
86103
StaticRouterStructure (Map Text RouterStructure) Int
87-
| CaptureRouterStructure RouterStructure
104+
| CaptureRouterStructure [CaptureHint] RouterStructure
88105
| RawRouterStructure
89106
| ChoiceStructure RouterStructure RouterStructure
90107
deriving (Eq, Show)
@@ -98,11 +115,11 @@ data RouterStructure =
98115
routerStructure :: Router' env a -> RouterStructure
99116
routerStructure (StaticRouter m ls) =
100117
StaticRouterStructure (fmap routerStructure m) (length ls)
101-
routerStructure (CaptureRouter router) =
102-
CaptureRouterStructure $
118+
routerStructure (CaptureRouter hints router) =
119+
CaptureRouterStructure hints $
103120
routerStructure router
104-
routerStructure (CaptureAllRouter router) =
105-
CaptureRouterStructure $
121+
routerStructure (CaptureAllRouter hints router) =
122+
CaptureRouterStructure hints $
106123
routerStructure router
107124
routerStructure (RawRouter _) =
108125
RawRouterStructure
@@ -111,11 +128,21 @@ routerStructure (Choice r1 r2) =
111128
(routerStructure r1)
112129
(routerStructure r2)
113130

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

120147
-- | Provide a textual representation of the
121148
-- structure of a router.
@@ -126,7 +153,8 @@ routerLayout router =
126153
where
127154
mkRouterLayout :: Bool -> RouterStructure -> [Text]
128155
mkRouterLayout c (StaticRouterStructure m n) = mkSubTrees c (M.toList m) n
129-
mkRouterLayout c (CaptureRouterStructure r) = mkSubTree c "<capture>" (mkRouterLayout False r)
156+
mkRouterLayout c (CaptureRouterStructure hints r) =
157+
mkSubTree c (toCaptureTags hints) (mkRouterLayout False r)
130158
mkRouterLayout c RawRouterStructure =
131159
if c then ["├─ <raw>"] else ["└─ <raw>"]
132160
mkRouterLayout c (ChoiceStructure r1 r2) =
@@ -169,15 +197,15 @@ runRouterEnv fmt router env request respond =
169197
-> let request' = request { pathInfo = rest }
170198
in runRouterEnv fmt router' env request' respond
171199
_ -> respond $ Fail $ fmt request
172-
CaptureRouter router' ->
200+
CaptureRouter _ router' ->
173201
case pathInfo request of
174202
[] -> respond $ Fail $ fmt request
175203
-- This case is to handle trailing slashes.
176204
[""] -> respond $ Fail $ fmt request
177205
first : rest
178206
-> let request' = request { pathInfo = rest }
179207
in runRouterEnv fmt router' (first, env) request' respond
180-
CaptureAllRouter router' ->
208+
CaptureAllRouter _ router' ->
181209
let segments = pathInfo request
182210
request' = request { pathInfo = [] }
183211
in runRouterEnv fmt router' (segments, env) request' respond

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Control.Monad
99
import Data.Proxy
1010
(Proxy (..))
1111
import Data.Text
12-
(unpack)
12+
(Text, unpack)
1313
import Network.HTTP.Types
1414
(Status (..))
1515
import Network.Wai
@@ -51,14 +51,17 @@ routerSpec = do
5151
toApp = toApplication . runRouter (const err404)
5252

5353
cap :: Router ()
54-
cap = CaptureRouter $
54+
cap = CaptureRouter [hint] $
5555
let delayed = addCapture (emptyDelayed $ Route pure) (const $ delayedFail err400)
5656
in leafRouter
5757
$ \env req res ->
5858
runAction delayed env req res
5959
. const
6060
$ Route success
6161

62+
hint :: CaptureHint
63+
hint = CaptureHint "anything" $ CaptureSingle
64+
6265
router :: Router ()
6366
router = leafRouter (\_ _ res -> res $ Route success)
6467
`Choice` cap

0 commit comments

Comments
 (0)