Skip to content

Commit 6ec4bdc

Browse files
committed
Merge pull request #451 from kosmikus/show-router
Improvements of router merging, visualization and testing
2 parents ba57d20 + 8c77882 commit 6ec4bdc

File tree

7 files changed

+527
-109
lines changed

7 files changed

+527
-109
lines changed

servant-server/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
0.7
2+
---
3+
4+
* The `Router` type has been changed. There are now more situations where
5+
servers will make use of static lookups to efficiently route the request
6+
to the correct endpoint. Functions `layout` and `layoutWithContext` have
7+
been added to visualize the router layout for debugging purposes. Test
8+
cases for expected router layouts have been added.
9+
110
0.6.1
211
-----
312

servant-server/src/Servant/Server.hs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ module Servant.Server
1818
HasServer(..)
1919
, Server
2020

21+
-- * Debugging the server layout
22+
, layout
23+
, layoutWithContext
24+
2125
-- * Enter
2226
-- $enterDoc
2327

@@ -93,6 +97,7 @@ module Servant.Server
9397
) where
9498

9599
import Data.Proxy (Proxy)
100+
import Data.Text (Text)
96101
import Network.Wai (Application)
97102
import Servant.Server.Internal
98103
import Servant.Server.Internal.Enter
@@ -131,6 +136,73 @@ serveWithContext p context server = toApplication (runRouter (route p context d)
131136
d = Delayed r r r r (\ _ _ _ -> Route server)
132137
r = return (Route ())
133138

139+
-- | The function 'layout' produces a textual description of the internal
140+
-- router layout for debugging purposes. Note that the router layout is
141+
-- determined just by the API, not by the handlers.
142+
--
143+
-- This function makes certain assumptions about the well-behavedness of
144+
-- the 'HasServer' instances of the combinators which should be ok for the
145+
-- core servant constructions, but might not be satisfied for some other
146+
-- combinators provided elsewhere. It is possible that the function may
147+
-- crash for these.
148+
--
149+
-- Example:
150+
--
151+
-- For the following API
152+
--
153+
-- > type API =
154+
-- > "a" :> "d" :> Get '[JSON] ()
155+
-- > :<|> "b" :> Capture "x" Int :> Get '[JSON] Bool
156+
-- > :<|> "c" :> Put '[JSON] Bool
157+
-- > :<|> "a" :> "e" :> Get '[JSON] Int
158+
-- > :<|> "b" :> Capture "x" Int :> Put '[JSON] Bool
159+
-- > :<|> Raw
160+
--
161+
-- we get the following output:
162+
--
163+
-- > /
164+
-- > ├─ a/
165+
-- > │ ├─ d/
166+
-- > │ │ └─•
167+
-- > │ └─ e/
168+
-- > │ └─•
169+
-- > ├─ b/
170+
-- > │ └─ <dyn>/
171+
-- > │ ├─•
172+
-- > │ ┆
173+
-- > │ └─•
174+
-- > ├─ c/
175+
-- > │ └─•
176+
-- > ┆
177+
-- > └─ <raw>
178+
--
179+
-- Explanation of symbols:
180+
--
181+
-- [@├@] Normal lines reflect static branching via a table.
182+
--
183+
-- [@a/@] Nodes reflect static path components.
184+
--
185+
-- [@─•@] Leaves reflect endpoints.
186+
--
187+
-- [@\<dyn\>/@] This is a delayed capture of a path component.
188+
--
189+
-- [@\<raw\>@] This is a part of the API we do not know anything about.
190+
--
191+
-- [@┆@] Dashed lines suggest a dynamic choice between the part above
192+
-- and below. If there is a success for fatal failure in the first part,
193+
-- that one takes precedence. If both parts fail, the \"better\" error
194+
-- code will be returned.
195+
--
196+
layout :: (HasServer layout '[]) => Proxy layout -> Text
197+
layout p = layoutWithContext p EmptyContext
198+
199+
-- | Variant of 'layout' that takes an additional 'Context'.
200+
layoutWithContext :: (HasServer layout context)
201+
=> Proxy layout -> Context context -> Text
202+
layoutWithContext p context = routerLayout (route p context d)
203+
where
204+
d = Delayed r r r r (\ _ _ _ -> FailFatal err501)
205+
r = return (Route ())
134206

135207
-- Documentation
136208

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

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,17 @@ import Control.Monad.Trans.Except (ExceptT)
2626
import qualified Data.ByteString as B
2727
import qualified Data.ByteString.Char8 as BC8
2828
import qualified Data.ByteString.Lazy as BL
29-
import qualified Data.Map as M
3029
import Data.Maybe (fromMaybe, mapMaybe)
3130
import Data.String (fromString)
3231
import Data.String.Conversions (cs, (<>))
33-
import Data.Text (Text)
3432
import Data.Typeable
3533
import GHC.TypeLits (KnownNat, KnownSymbol, natVal,
3634
symbolVal)
3735
import Network.HTTP.Types hiding (Header, ResponseHeaders)
3836
import Network.Socket (SockAddr)
3937
import Network.Wai (Application, Request, Response,
4038
httpVersion, isSecure,
41-
lazyRequestBody, pathInfo,
39+
lazyRequestBody,
4240
rawQueryString, remoteHost,
4341
requestHeaders, requestMethod,
4442
responseLBS, vault)
@@ -161,34 +159,30 @@ methodRouter :: (AllCTRender ctypes a)
161159
=> Method -> Proxy ctypes -> Status
162160
-> Delayed (ExceptT ServantErr IO a)
163161
-> Router
164-
methodRouter method proxy status action = LeafRouter route'
162+
methodRouter method proxy status action = leafRouter route'
165163
where
166-
route' request respond
167-
| pathIsEmpty request =
164+
route' request respond =
168165
let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
169166
in runAction (action `addMethodCheck` methodCheck method request
170167
`addAcceptCheck` acceptCheck proxy accH
171168
) respond $ \ output -> do
172169
let handleA = handleAcceptH proxy (AcceptHeader accH) output
173170
processMethodRouter handleA status method Nothing request
174-
| otherwise = respond $ Fail err404
175171

176172
methodRouterHeaders :: (GetHeaders (Headers h v), AllCTRender ctypes v)
177173
=> Method -> Proxy ctypes -> Status
178174
-> Delayed (ExceptT ServantErr IO (Headers h v))
179175
-> Router
180-
methodRouterHeaders method proxy status action = LeafRouter route'
176+
methodRouterHeaders method proxy status action = leafRouter route'
181177
where
182-
route' request respond
183-
| pathIsEmpty request =
178+
route' request respond =
184179
let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
185180
in runAction (action `addMethodCheck` methodCheck method request
186181
`addAcceptCheck` acceptCheck proxy accH
187182
) respond $ \ output -> do
188183
let headers = getHeaders output
189184
handleA = handleAcceptH proxy (AcceptHeader accH) (getResponse output)
190185
processMethodRouter handleA status method (Just headers) request
191-
| otherwise = respond $ Fail err404
192186

193187
instance OVERLAPPABLE_
194188
( AllCTRender ctypes a, ReflectMethod method, KnownNat status
@@ -359,7 +353,7 @@ instance HasServer Raw context where
359353

360354
type ServerT Raw m = Application
361355

362-
route Proxy _ rawApplication = LeafRouter $ \ request respond -> do
356+
route Proxy _ rawApplication = RawRouter $ \ request respond -> do
363357
r <- runDelayed rawApplication
364358
case r of
365359
Route app -> app request (respond . Route)
@@ -416,9 +410,10 @@ instance (KnownSymbol path, HasServer sublayout context) => HasServer (path :> s
416410

417411
type ServerT (path :> sublayout) m = ServerT sublayout m
418412

419-
route Proxy context subserver = StaticRouter $
420-
M.singleton (cs (symbolVal proxyPath))
421-
(route (Proxy :: Proxy sublayout) context subserver)
413+
route Proxy context subserver =
414+
pathRouter
415+
(cs (symbolVal proxyPath))
416+
(route (Proxy :: Proxy sublayout) context subserver)
422417
where proxyPath = Proxy :: Proxy path
423418

424419
instance HasServer api context => HasServer (RemoteHost :> api) context where
@@ -465,12 +460,6 @@ instance ( KnownSymbol realm
465460

466461
-- * helpers
467462

468-
pathIsEmpty :: Request -> Bool
469-
pathIsEmpty = go . pathInfo
470-
where go [] = True
471-
go [""] = True
472-
go _ = False
473-
474463
ct_wildcard :: B.ByteString
475464
ct_wildcard = "*" <> "/" <> "*" -- Because CPP
476465

0 commit comments

Comments
 (0)