git.iko.soy/typeable/servant
on master help sign in
Improvements and visualization of router structure.

* Improves how Routers are built, in particular via the `choice` smart constructors. Static lookups are now used more often. * We now have test cases making sure that certain routers have the same structure. * The router structure can now be visualized for debugging purposes as a tree. The new functions `layout` and `layoutWithContext` do this.

commit8c778825c71dd6a5d594fada9f855d47a3897f58 authorAndres Loeh <andres@well-typed.com> dateThu 7 Apr 2016, 13:45 (11 years ago) parentba57d20
7 files changed+527−109
servant-server/CHANGELOG.md+9 servant-server/src/Servant/Server.hs+72 servant-server/src/Servant/Server/Internal.hs+10−21 servant-server/src/Servant/Server/Internal/Router.hs+140−30 servant-server/src/Servant/Server/Internal/RoutingApplication.hs−25 servant-server/test/Servant/Server/RouterSpec.hs+295 servant-server/test/Servant/ServerSpec.hs+1−33
servant-server/CHANGELOG.md+9
@@ -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+
1100.6.1
211-----
312
servant-server/src/Servant/Server.hs+72
@@ -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
9599import Data.Proxy (Proxy)
100+import Data.Text (Text)
96101import Network.Wai (Application)
97102import Servant.Server.Internal
98103import 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+10−21
@@ -26,11 +26,9 @@ import Control.Monad.Trans.Except (ExceptT)
2626import qualified Data.ByteString as B
2727import qualified Data.ByteString.Char8 as BC8
2828import qualified Data.ByteString.Lazy as BL
29−import qualified Data.Map as M
3029import Data.Maybe (fromMaybe, mapMaybe)
3130import Data.String (fromString)
3231import Data.String.Conversions (cs, (<>))
33−import Data.Text (Text)
3432import Data.Typeable
3533import GHC.TypeLits (KnownNat, KnownSymbol, natVal,
3634 symbolVal)
@@ -38,7 +36,7 @@ import Network.HTTP.Types hiding (Header, ResponseHeaders)
3836import Network.Socket (SockAddr)
3937import Network.Wai (Application, Request, Response,
4038 httpVersion, isSecure,
41− lazyRequestBody, pathInfo,
39+ lazyRequestBody,
4240 rawQueryString, remoteHost,
4341 requestHeaders, requestMethod,
4442 responseLBS, vault)
@@ -161,26 +159,23 @@ 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
176172methodRouterHeaders :: (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
@@ -188,7 +183,6 @@ methodRouterHeaders method proxy status action = LeafRouter route'
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
193187instance 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
424419instance 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−
474463ct_wildcard :: B.ByteString
475464ct_wildcard = "*" <> "/" <> "*" -- Because CPP
476465
servant-server/src/Servant/Server/Internal/Router.hs+140−30
@@ -1,10 +1,13 @@
11{-# LANGUAGE DeriveFunctor #-}
22{-# LANGUAGE CPP #-}
3+{-# LANGUAGE OverloadedStrings #-}
34module Servant.Server.Internal.Router where
45
56import Data.Map (Map)
67import qualified Data.Map as M
8+import Data.Monoid
79import Data.Text (Text)
10+import qualified Data.Text as T
811import Network.Wai (Request, Response, pathInfo)
912import Servant.Server.Internal.RoutingApplication
1013import Servant.Server.Internal.ServantErr
@@ -13,36 +16,46 @@ type Router = Router' RoutingApplication
1316
1417-- | Internal representation of a router.
1518data Router' a =
16− WithRequest (Request -> Router)
19+ WithRequest (Request -> Router' a)
1720 -- ^ current request is passed to the router
18− | StaticRouter (Map Text Router)
19− -- ^ first path component used for lookup and removed afterwards
20− | DynamicRouter (Text -> Router)
21− -- ^ first path component used for lookup and removed afterwards
22− | LeafRouter a
23− -- ^ to be used for routes that match an empty path
24− | Choice Router Router
21+ | StaticRouter (Map Text (Router' a)) [a]
22+ -- ^ the map contains routers for subpaths (first path component used
23+ -- for lookup and removed afterwards), the list contains handlers
24+ -- for the empty path, to be tried in order
25+ | DynamicRouter (Text -> Router' a)
26+ -- ^ first path component passed to the function and removed afterwards
27+ | RawRouter a
28+ -- ^ to be used for routes we do not know anything about
29+ | Choice (Router' a) (Router' a)
2530 -- ^ left-biased choice between two routers
2631 deriving Functor
2732
28−-- | Apply a transformation to the response of a `Router`.
29−tweakResponse :: (RouteResult Response -> RouteResult Response) -> Router -> Router
30−tweakResponse f = fmap (\a -> \req cont -> a req (cont . f))
33+-- | Smart constructor for a single static path component.
34+pathRouter :: Text -> Router' a -> Router' a
35+pathRouter t r = StaticRouter (M.singleton t r) []
36+
37+-- | Smart constructor for a leaf, i.e., a router that expects
38+-- the empty path.
39+--
40+leafRouter :: a -> Router' a
41+leafRouter l = StaticRouter M.empty [l]
3142
3243-- | Smart constructor for the choice between routers.
3344-- We currently optimize the following cases:
3445--
35−-- * Two static routers can be joined by joining their maps.
46+-- * Two static routers can be joined by joining their maps
47+-- and concatenating their leaf-lists.
3648-- * Two dynamic routers can be joined by joining their codomains.
3749-- * Two 'WithRequest' routers can be joined by passing them
3850-- the same request and joining their codomains.
3951-- * A 'WithRequest' router can be joined with anything else by
4052-- passing the same request to both but ignoring it in the
4153-- component that does not need it.
54+-- * Choice nodes can be reordered.
4255--
4356choice :: Router -> Router -> Router
44−choice (StaticRouter table1) (StaticRouter table2) =
45− StaticRouter (M.unionWith choice table1 table2)
57+choice (StaticRouter table1 ls1) (StaticRouter table2 ls2) =
58+ StaticRouter (M.unionWith choice table1 table2) (ls1 ++ ls2)
4659choice (DynamicRouter fun1) (DynamicRouter fun2) =
4760 DynamicRouter (\ first -> choice (fun1 first) (fun2 first))
4861choice (WithRequest router1) (WithRequest router2) =
@@ -51,39 +64,136 @@ choice (WithRequest router1) router2 =
5164 WithRequest (\ request -> choice (router1 request) router2)
5265choice router1 (WithRequest router2) =
5366 WithRequest (\ request -> choice router1 (router2 request))
67+choice router1 (Choice router2 router3) = Choice (choice router1 router2) router3
5468choice router1 router2 = Choice router1 router2
5569
70+-- | Datatype used for representing and debugging the
71+-- structure of a router. Abstracts from the functions
72+-- being used in the actual router and the handlers at
73+-- the leaves.
74+--
75+-- Two 'Router's can be structurally compared by computing
76+-- their 'RouterStructure' using 'routerStructure' and
77+-- then testing for equality, see 'sameStructure'.
78+--
79+data RouterStructure =
80+ WithRequestStructure RouterStructure
81+ | StaticRouterStructure (Map Text RouterStructure) Int
82+ | DynamicRouterStructure RouterStructure
83+ | RawRouterStructure
84+ | ChoiceStructure RouterStructure RouterStructure
85+ deriving (Eq, Show)
86+
87+-- | Compute the structure of a router.
88+--
89+-- Assumes that the request or text being passed
90+-- in 'WithRequest' or 'DynamicRouter' does not
91+-- affect the structure of the underlying tree.
92+--
93+routerStructure :: Router' a -> RouterStructure
94+routerStructure (WithRequest f) =
95+ WithRequestStructure $
96+ routerStructure (f (error "routerStructure: dummy request"))
97+routerStructure (StaticRouter m ls) =
98+ StaticRouterStructure (fmap routerStructure m) (length ls)
99+routerStructure (DynamicRouter f) =
100+ DynamicRouterStructure $
101+ routerStructure (f (error "routerStructure: dummy text"))
102+routerStructure (RawRouter _) =
103+ RawRouterStructure
104+routerStructure (Choice r1 r2) =
105+ ChoiceStructure
106+ (routerStructure r1)
107+ (routerStructure r2)
108+
109+-- | Compare the structure of two routers.
110+--
111+sameStructure :: Router' a -> Router' b -> Bool
112+sameStructure r1 r2 =
113+ routerStructure r1 == routerStructure r2
114+
115+-- | Provide a textual representation of the
116+-- structure of a router.
117+--
118+routerLayout :: Router' a -> Text
119+routerLayout router =
120+ T.unlines (["/"] ++ mkRouterLayout False (routerStructure router))
121+ where
122+ mkRouterLayout :: Bool -> RouterStructure -> [Text]
123+ mkRouterLayout c (WithRequestStructure r) = mkRouterLayout c r
124+ mkRouterLayout c (StaticRouterStructure m n) = mkSubTrees c (M.toList m) n
125+ mkRouterLayout c (DynamicRouterStructure r) = mkSubTree c "<dyn>" (mkRouterLayout False r)
126+ mkRouterLayout c RawRouterStructure =
127+ if c then ["├─ <raw>"] else ["└─ <raw>"]
128+ mkRouterLayout c (ChoiceStructure r1 r2) =
129+ mkRouterLayout True r1 ++ ["┆"] ++ mkRouterLayout c r2
130+
131+ mkSubTrees :: Bool -> [(Text, RouterStructure)] -> Int -> [Text]
132+ mkSubTrees _ [] 0 = []
133+ mkSubTrees c [] n =
134+ concat (replicate (n - 1) (mkLeaf True) ++ [mkLeaf c])
135+ mkSubTrees c [(t, r)] 0 =
136+ mkSubTree c t (mkRouterLayout False r)
137+ mkSubTrees c ((t, r) : trs) n =
138+ mkSubTree True t (mkRouterLayout False r) ++ mkSubTrees c trs n
139+
140+ mkLeaf :: Bool -> [Text]
141+ mkLeaf True = ["├─•","┆"]
142+ mkLeaf False = ["└─•"]
143+
144+ mkSubTree :: Bool -> Text -> [Text] -> [Text]
145+ mkSubTree True path children = ("├─ " <> path <> "/") : map ("│ " <>) children
146+ mkSubTree False path children = ("└─ " <> path <> "/") : map (" " <>) children
147+
148+-- | Apply a transformation to the response of a `Router`.
149+tweakResponse :: (RouteResult Response -> RouteResult Response) -> Router -> Router
150+tweakResponse f = fmap (\a -> \req cont -> a req (cont . f))
151+
56152-- | Interpret a router as an application.
57153runRouter :: Router -> RoutingApplication
58154runRouter (WithRequest router) request respond =
59155 runRouter (router request) request respond
60−runRouter (StaticRouter table) request respond =
156+runRouter (StaticRouter table ls) request respond =
61157 case pathInfo request of
62− first : rest
63− | Just router <- M.lookup first table
158+ [] -> runChoice ls request respond
159+ -- This case is to handle trailing slashes.
160+ [""] -> runChoice ls request respond
161+ first : rest | Just router <- M.lookup first table
64162 -> let request' = request { pathInfo = rest }
65163 in runRouter router request' respond
66164 _ -> respond $ Fail err404
67165runRouter (DynamicRouter fun) request respond =
68166 case pathInfo request of
167+ [] -> respond $ Fail err404
168+ -- This case is to handle trailing slashes.
169+ [""] -> respond $ Fail err404
69170 first : rest
70171 -> let request' = request { pathInfo = rest }
71172 in runRouter (fun first) request' respond
72− _ -> respond $ Fail err404
73−runRouter (LeafRouter app) request respond = app request respond
173+runRouter (RawRouter app) request respond = app request respond
74174runRouter (Choice r1 r2) request respond =
75− runRouter r1 request $ \ mResponse1 -> case mResponse1 of
76− Fail _ -> runRouter r2 request $ \ mResponse2 ->
77− respond (highestPri mResponse1 mResponse2)
78− _ -> respond mResponse1
79− where
80− highestPri (Fail e1) (Fail e2) =
81− if worseHTTPCode (errHTTPCode e1) (errHTTPCode e2)
82− then Fail e2
83− else Fail e1
84− highestPri (Fail _) y = y
85− highestPri x _ = x
175+ runChoice [runRouter r1, runRouter r2] request respond
86176
177+-- | Try a list of routing applications in order.
178+-- We stop as soon as one fails fatally or succeeds.
179+-- If all fail normally, we pick the "best" error.
180+--
181+runChoice :: [RoutingApplication] -> RoutingApplication
182+runChoice [] _request respond = respond (Fail err404)
183+runChoice [r] request respond = r request respond
184+runChoice (r : rs) request respond =
185+ r request $ \ response1 ->
186+ case response1 of
187+ Fail _ -> runChoice rs request $ \ response2 ->
188+ respond $ highestPri response1 response2
189+ _ -> respond response1
190+ where
191+ highestPri (Fail e1) (Fail e2) =
192+ if worseHTTPCode (errHTTPCode e1) (errHTTPCode e2)
193+ then Fail e2
194+ else Fail e1
195+ highestPri (Fail _) y = y
196+ highestPri x _ = x
87197
88198-- Priority on HTTP codes.
89199--
servant-server/src/Servant/Server/Internal/RoutingApplication.hs−25
@@ -35,31 +35,6 @@ toApplication ra request respond = ra request routingRespond
3535 routingRespond (FailFatal err) = respond $ responseServantErr err
3636 routingRespond (Route v) = respond v
3737
38−-- We currently mix up the order in which we perform checks
39−-- and the priority with which errors are reported.
40−--
41−-- For example, we perform Capture checks prior to method checks,
42−-- and therefore get 404 before 405.
43−--
44−-- However, we also perform body checks prior to method checks
45−-- now, and therefore get 415 before 405, which is wrong.
46−--
47−-- If we delay Captures, but perform method checks eagerly, we
48−-- end up potentially preferring 405 over 404, which is also bad.
49−--
50−-- So in principle, we'd like:
51−--
52−-- static routes (can cause 404)
53−-- delayed captures (can cause 404)
54−-- methods (can cause 405)
55−-- authentication and authorization (can cause 401, 403)
56−-- delayed body (can cause 415, 400)
57−-- accept header (can cause 406)
58−--
59−-- According to the HTTP decision diagram, the priority order
60−-- between HTTP status codes is as follows:
61−--
62−
6338-- | A 'Delayed' is a representation of a handler with scheduled
6439-- delayed checks that can trigger errors.
6540--
servant-server/test/Servant/Server/RouterSpec.hsnew file+295
@@ -0,0 +1,295 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE TypeOperators #-}
5+module Servant.Server.RouterSpec (spec) where
6+
7+import Control.Monad (unless)
8+import Data.Proxy (Proxy(..))
9+import Data.Text (unpack)
10+import Network.HTTP.Types (Status (..))
11+import Network.Wai (Application, responseBuilder)
12+import Network.Wai.Internal (Response (ResponseBuilder))
13+import Test.Hspec
14+import Test.Hspec.Wai (get, shouldRespondWith, with)
15+import Servant.API
16+import Servant.Server
17+import Servant.Server.Internal
18+
19+spec = describe "Servant.Server.Internal.Router" $ do
20+ routerSpec
21+ distributivitySpec
22+
23+routerSpec :: Spec
24+routerSpec = do
25+ let app' :: Application
26+ app' = toApplication $ runRouter router'
27+
28+ router', router :: Router
29+ router' = tweakResponse (fmap twk) router
30+ router = leafRouter $ \_ cont -> cont (Route $ responseBuilder (Status 201 "") [] "")
31+
32+ twk :: Response -> Response
33+ twk (ResponseBuilder (Status i s) hs b) = ResponseBuilder (Status (i + 1) s) hs b
34+ twk b = b
35+
36+ describe "tweakResponse" . with (return app') $ do
37+ it "calls f on route result" $ do
38+ get "" `shouldRespondWith` 202
39+
40+distributivitySpec :: Spec
41+distributivitySpec =
42+ describe "choice" $ do
43+ it "distributes endpoints through static paths" $ do
44+ endpoint `shouldHaveSameStructureAs` endpointRef
45+ it "distributes nested routes through static paths" $ do
46+ static `shouldHaveSameStructureAs` staticRef
47+ it "distributes nested routes through dynamic paths" $ do
48+ dynamic `shouldHaveSameStructureAs` dynamicRef
49+ it "properly reorders permuted static paths" $ do
50+ permute `shouldHaveSameStructureAs` permuteRef
51+ it "properly reorders permuted static paths in the presence of Raw in end" $ do
52+ permuteRawEnd `shouldHaveSameStructureAs` permuteRawEndRef
53+ it "properly reorders permuted static paths in the presence of Raw in beginning" $ do
54+ permuteRawBegin `shouldHaveSameStructureAs` permuteRawBeginRef
55+ it "properly reorders permuted static paths in the presence of Raw in middle" $ do
56+ permuteRawMiddle `shouldHaveSameStructureAs` permuteRawMiddleRef
57+ it "properly reorders permuted static paths in the presence of a root endpoint in end" $ do
58+ permuteEndEnd `shouldHaveSameStructureAs` permuteEndRef
59+ it "properly reorders permuted static paths in the presence of a root endpoint in beginning" $ do
60+ permuteEndBegin `shouldHaveSameStructureAs` permuteEndRef
61+ it "properly reorders permuted static paths in the presence of a root endpoint in middle" $ do
62+ permuteEndMiddle `shouldHaveSameStructureAs` permuteEndRef
63+ it "properly handles mixing static paths at different levels" $ do
64+ level `shouldHaveSameStructureAs` levelRef
65+
66+shouldHaveSameStructureAs ::
67+ (HasServer api1 '[], HasServer api2 '[]) => Proxy api1 -> Proxy api2 -> Expectation
68+shouldHaveSameStructureAs p1 p2 =
69+ unless (sameStructure (makeTrivialRouter p1) (makeTrivialRouter p2)) $
70+ expectationFailure ("expected:\n" ++ unpack (layout p2) ++ "\nbut got:\n" ++ unpack (layout p1))
71+
72+makeTrivialRouter :: (HasServer layout '[]) => Proxy layout -> Router
73+makeTrivialRouter p = route p EmptyContext d
74+ where
75+ d = Delayed r r r r (\ _ _ _ -> FailFatal err501)
76+ r = return (Route ())
77+
78+type End = Get '[JSON] ()
79+
80+-- The latter version looks more efficient,
81+-- but the former should be compiled to the
82+-- same layout:
83+
84+type Endpoint = "a" :> End :<|> "a" :> End
85+type EndpointRef = "a" :> (End :<|> End)
86+
87+endpoint :: Proxy Endpoint
88+endpoint = Proxy
89+
90+endpointRef :: Proxy EndpointRef
91+endpointRef = Proxy
92+
93+-- Again, the latter version looks more efficient,
94+-- but the former should be compiled to the same
95+-- layout:
96+
97+type Static = "a" :> "b" :> End :<|> "a" :> "c" :> End
98+type StaticRef = "a" :> ("b" :> End :<|> "c" :> End)
99+
100+static :: Proxy Static
101+static = Proxy
102+
103+staticRef :: Proxy StaticRef
104+staticRef = Proxy
105+
106+-- Even for dynamic path components, we expect the
107+-- router to simplify the layout, because captures
108+-- are delayed and only actually performed once
109+-- reaching an endpoint. So the former version and
110+-- the latter should be compiled to the same router
111+-- structure:
112+
113+type Dynamic =
114+ "a" :> Capture "foo" Int :> "b" :> End
115+ :<|> "a" :> Capture "bar" Bool :> "c" :> End
116+ :<|> "a" :> Capture "baz" Char :> "d" :> End
117+
118+type DynamicRef =
119+ "a" :> Capture "anything" () :>
120+ ("b" :> End :<|> "c" :> End :<|> "d" :> End)
121+
122+dynamic :: Proxy Dynamic
123+dynamic = Proxy
124+
125+dynamicRef :: Proxy DynamicRef
126+dynamicRef = Proxy
127+
128+-- A more complicated example of static route reordering.
129+-- All the permuted paths should be correctly grouped,
130+-- so both 'Permute' and 'PermuteRef' should compile to
131+-- the same layout:
132+
133+type Permute =
134+ "a" :> "b" :> "c" :> End
135+ :<|> "b" :> "a" :> "c" :> End
136+ :<|> "a" :> "c" :> "b" :> End
137+ :<|> "c" :> "a" :> "b" :> End
138+ :<|> "b" :> "c" :> "a" :> End
139+ :<|> "c" :> "b" :> "a" :> End
140+
141+type PermuteRef =
142+ "a" :> ( "b" :> "c" :> End
143+ :<|> "c" :> "b" :> End
144+ )
145+ :<|> "b" :> ( "a" :> "c" :> End
146+ :<|> "c" :> "a" :> End
147+ )
148+ :<|> "c" :> ( "a" :> "b" :> End
149+ :<|> "b" :> "a" :> End
150+ )
151+
152+permute :: Proxy Permute
153+permute = Proxy
154+
155+permuteRef :: Proxy PermuteRef
156+permuteRef = Proxy
157+
158+-- Adding a 'Raw' in one of the ends should have minimal
159+-- effect on the grouping.
160+
161+type PermuteRawEnd =
162+ "a" :> "b" :> "c" :> End
163+ :<|> "b" :> "a" :> "c" :> End
164+ :<|> "a" :> "c" :> "b" :> End
165+ :<|> "c" :> "a" :> "b" :> End
166+ :<|> "b" :> "c" :> "a" :> End
167+ :<|> "c" :> "b" :> "a" :> End
168+ :<|> Raw
169+
170+type PermuteRawEndRef = PermuteRef :<|> Raw
171+
172+type PermuteRawBegin =
173+ Raw
174+ :<|> "a" :> "b" :> "c" :> End
175+ :<|> "b" :> "a" :> "c" :> End
176+ :<|> "a" :> "c" :> "b" :> End
177+ :<|> "c" :> "a" :> "b" :> End
178+ :<|> "b" :> "c" :> "a" :> End
179+ :<|> "c" :> "b" :> "a" :> End
180+
181+type PermuteRawBeginRef = Raw :<|> PermuteRef
182+
183+permuteRawBegin :: Proxy PermuteRawBegin
184+permuteRawBegin = Proxy
185+
186+permuteRawBeginRef :: Proxy PermuteRawBeginRef
187+permuteRawBeginRef = Proxy
188+
189+permuteRawEnd :: Proxy PermuteRawEnd
190+permuteRawEnd = Proxy
191+
192+permuteRawEndRef :: Proxy PermuteRawEndRef
193+permuteRawEndRef = Proxy
194+
195+-- Adding a 'Raw' in the middle will disrupt grouping,
196+-- because we commute things past a 'Raw'. But the two
197+-- halves should still be grouped.
198+
199+type PermuteRawMiddle =
200+ "a" :> "b" :> "c" :> End
201+ :<|> "b" :> "a" :> "c" :> End
202+ :<|> "a" :> "c" :> "b" :> End
203+ :<|> Raw
204+ :<|> "c" :> "a" :> "b" :> End
205+ :<|> "b" :> "c" :> "a" :> End
206+ :<|> "c" :> "b" :> "a" :> End
207+
208+type PermuteRawMiddleRef =
209+ "a" :> ( "b" :> "c" :> End
210+ :<|> "c" :> "b" :> End
211+ )
212+ :<|> "b" :> "a" :> "c" :> End
213+ :<|> Raw
214+ :<|> "b" :> "c" :> "a" :> End
215+ :<|> "c" :> ( "a" :> "b" :> End
216+ :<|> "b" :> "a" :> End
217+ )
218+
219+permuteRawMiddle :: Proxy PermuteRawMiddle
220+permuteRawMiddle = Proxy
221+
222+permuteRawMiddleRef :: Proxy PermuteRawMiddleRef
223+permuteRawMiddleRef = Proxy
224+
225+-- Adding an endpoint at the top-level in various places
226+-- is also somewhat critical for grouping, but it should
227+-- not disrupt grouping at all, even if it is placed in
228+-- the middle.
229+
230+type PermuteEndEnd =
231+ "a" :> "b" :> "c" :> End
232+ :<|> "b" :> "a" :> "c" :> End
233+ :<|> "a" :> "c" :> "b" :> End
234+ :<|> "c" :> "a" :> "b" :> End
235+ :<|> "b" :> "c" :> "a" :> End
236+ :<|> "c" :> "b" :> "a" :> End
237+ :<|> End
238+
239+type PermuteEndBegin =
240+ End
241+ :<|> "a" :> "b" :> "c" :> End
242+ :<|> "b" :> "a" :> "c" :> End
243+ :<|> "a" :> "c" :> "b" :> End
244+ :<|> "c" :> "a" :> "b" :> End
245+ :<|> "b" :> "c" :> "a" :> End
246+ :<|> "c" :> "b" :> "a" :> End
247+
248+type PermuteEndMiddle =
249+ "a" :> "b" :> "c" :> End
250+ :<|> "b" :> "a" :> "c" :> End
251+ :<|> "a" :> "c" :> "b" :> End
252+ :<|> End
253+ :<|> "c" :> "a" :> "b" :> End
254+ :<|> "b" :> "c" :> "a" :> End
255+ :<|> "c" :> "b" :> "a" :> End
256+
257+type PermuteEndRef = PermuteRef :<|> End
258+
259+permuteEndEnd :: Proxy PermuteEndEnd
260+permuteEndEnd = Proxy
261+
262+permuteEndBegin :: Proxy PermuteEndBegin
263+permuteEndBegin = Proxy
264+
265+permuteEndMiddle :: Proxy PermuteEndMiddle
266+permuteEndMiddle = Proxy
267+
268+permuteEndRef :: Proxy PermuteEndRef
269+permuteEndRef = Proxy
270+
271+-- An API with routes on different nesting levels that
272+-- is composed out of different fragments should still
273+-- be reordered correctly.
274+
275+type LevelFragment1 =
276+ "a" :> "b" :> End
277+ :<|> "a" :> End
278+
279+type LevelFragment2 =
280+ "b" :> End
281+ :<|> "a" :> "c" :> End
282+ :<|> End
283+
284+type Level = LevelFragment1 :<|> LevelFragment2
285+
286+type LevelRef =
287+ "a" :> ("b" :> End :<|> "c" :> End :<|> End)
288+ :<|> "b" :> End
289+ :<|> End
290+
291+level :: Proxy Level
292+level = Proxy
293+
294+levelRef :: Proxy LevelRef
295+levelRef = Proxy
servant-server/test/Servant/ServerSpec.hs+1−33
@@ -13,9 +13,6 @@
1313
1414module Servant.ServerSpec where
1515
16−#if !MIN_VERSION_base(4,8,0)
17−import Control.Applicative ((<$>))
18−#endif
1916import Control.Monad (forM_, when, unless)
2017import Control.Monad.Trans.Except (ExceptT, throwE)
2118import Data.Aeson (FromJSON, ToJSON, decode', encode)
@@ -36,8 +33,7 @@ import Network.HTTP.Types (Status (..), hAccept, hContentType,
3633 parseQuery)
3734import Network.Wai (Application, Request, requestHeaders, pathInfo,
3835 queryString, rawQueryString,
39− responseBuilder, responseLBS)
40−import Network.Wai.Internal (Response (ResponseBuilder))
36+ responseLBS)
4137import Network.Wai.Test (defaultRequest, request,
4238 runSession, simpleBody,
4339 simpleHeaders, simpleStatus)
@@ -66,11 +62,6 @@ import Servant.Server.Internal.BasicAuth (BasicAuthCheck(BasicAuthChec
6662import Servant.Server.Experimental.Auth
6763 (AuthHandler, AuthServerData,
6864 mkAuthHandler)
69−import Servant.Server.Internal.RoutingApplication
70− (toApplication, RouteResult(..))
71−import Servant.Server.Internal.Router
72− (tweakResponse, runRouter,
73− Router, Router'(LeafRouter))
7465import Servant.Server.Internal.Context
7566 (NamedContext(..))
7667
@@ -94,7 +85,6 @@ spec = do
9485 rawSpec
9586 alternativeSpec
9687 responseHeadersSpec
97− routerSpec
9888 miscCombinatorSpec
9989 basicAuthSpec
10090 genAuthSpec
@@ -482,28 +472,6 @@ responseHeadersSpec = describe "ResponseHeaders" $ do
482472 THW.request method "" [(hAccept, "crazy/mime")] ""
483473 `shouldRespondWith` 406
484474
485−-- }}}
486−------------------------------------------------------------------------------
487−-- * routerSpec {{{
488−------------------------------------------------------------------------------
489−routerSpec :: Spec
490−routerSpec = do
491− describe "Servant.Server.Internal.Router" $ do
492− let app' :: Application
493− app' = toApplication $ runRouter router'
494−
495− router', router :: Router
496− router' = tweakResponse (twk <$>) router
497− router = LeafRouter $ \_ cont -> cont (Route $ responseBuilder (Status 201 "") [] "")
498−
499− twk :: Response -> Response
500− twk (ResponseBuilder (Status i s) hs b) = ResponseBuilder (Status (i + 1) s) hs b
501− twk b = b
502−
503− describe "tweakResponse" . with (return app') $ do
504− it "calls f on route result" $ do
505− get "" `shouldRespondWith` 202
506−
507475-- }}}
508476------------------------------------------------------------------------------
509477-- * miscCombinatorSpec {{{