@@ -1,10 +1,13 @@
1{-# LANGUAGE DeriveFunctor #-}
1{-# LANGUAGE DeriveFunctor #-}
2{-# LANGUAGE CPP #-}
2{-# LANGUAGE CPP #-}
3+{-# LANGUAGE OverloadedStrings #-}
3module Servant.Server.Internal.Router where
4module Servant.Server.Internal.Router where
5import Data.Map (Map)
6import Data.Map (Map)
6import qualified Data.Map as M
7import qualified Data.Map as M
7import Data.Text (Text)
9import Data.Text (Text)
10+import qualified Data.Text as T
8import Network.Wai (Request, Response, pathInfo)
11import Network.Wai (Request, Response, pathInfo)
9import Servant.Server.Internal.RoutingApplication
12import Servant.Server.Internal.RoutingApplication
10import Servant.Server.Internal.ServantErr
13import Servant.Server.Internal.ServantErr
@@ -13,36 +16,46 @@ type Router = Router' RoutingApplication
14-- | Internal representation of a router.
17-- | Internal representation of a router.
15data Router' a =
18data Router' a =
16− WithRequest (Request -> Router)
19+ WithRequest (Request -> Router' a)
17 -- ^ current request is passed to the router
20 -- ^ current request is passed to the router
18− | StaticRouter (Map Text Router)
21+ | StaticRouter (Map Text (Router' a)) [a]
22+ -- ^ the map contains routers for subpaths (first path component used
19− -- ^ first path component used for lookup and removed afterwards
23+ -- for lookup and removed afterwards), the list contains handlers
24+ -- for the empty path, to be tried in order
20− | DynamicRouter (Text -> Router)
25+ | DynamicRouter (Text -> Router' a)
21− -- ^ first path component used for lookup and removed afterwards
26+ -- ^ first path component passed to the function and removed afterwards
22− | LeafRouter a
27+ | RawRouter a
23− -- ^ to be used for routes that match an empty path
28+ -- ^ to be used for routes we do not know anything about
24− | Choice Router Router
29+ | Choice (Router' a) (Router' a)
25 -- ^ left-biased choice between two routers
30 -- ^ left-biased choice between two routers
26 deriving Functor
31 deriving Functor
28−-- | Apply a transformation to the response of a `Router`.
33+-- | Smart constructor for a single static path component.
29−tweakResponse :: (RouteResult Response -> RouteResult Response) -> Router -> Router
34+pathRouter :: Text -> Router' a -> Router' a
30−tweakResponse f = fmap (\a -> \req cont -> a req (cont . f))
35+pathRouter t r = StaticRouter (M.singleton t r) []
37+-- | Smart constructor for a leaf, i.e., a router that expects
40+leafRouter :: a -> Router' a
41+leafRouter l = StaticRouter M.empty [l]
32-- | Smart constructor for the choice between routers.
43-- | Smart constructor for the choice between routers.
33-- We currently optimize the following cases:
44-- We currently optimize the following cases:
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.
36-- * Two dynamic routers can be joined by joining their codomains.
48-- * Two dynamic routers can be joined by joining their codomains.
37-- * Two 'WithRequest' routers can be joined by passing them
49-- * Two 'WithRequest' routers can be joined by passing them
38-- the same request and joining their codomains.
50-- the same request and joining their codomains.
39-- * A 'WithRequest' router can be joined with anything else by
51-- * A 'WithRequest' router can be joined with anything else by
40-- passing the same request to both but ignoring it in the
52-- passing the same request to both but ignoring it in the
41-- component that does not need it.
53-- component that does not need it.
54+-- * Choice nodes can be reordered.
43choice :: Router -> Router -> Router
56choice :: Router -> Router -> Router
44−choice (StaticRouter table1) (StaticRouter table2) =
57+choice (StaticRouter table1 ls1) (StaticRouter table2 ls2) =
45− StaticRouter (M.unionWith choice table1 table2)
58+ StaticRouter (M.unionWith choice table1 table2) (ls1 ++ ls2)
46choice (DynamicRouter fun1) (DynamicRouter fun2) =
59choice (DynamicRouter fun1) (DynamicRouter fun2) =
47 DynamicRouter (\ first -> choice (fun1 first) (fun2 first))
60 DynamicRouter (\ first -> choice (fun1 first) (fun2 first))
48choice (WithRequest router1) (WithRequest router2) =
61choice (WithRequest router1) (WithRequest router2) =
@@ -51,39 +64,136 @@ choice (WithRequest router1) router2 =
51 WithRequest (\ request -> choice (router1 request) router2)
64 WithRequest (\ request -> choice (router1 request) router2)
52choice router1 (WithRequest router2) =
65choice router1 (WithRequest router2) =
53 WithRequest (\ request -> choice router1 (router2 request))
66 WithRequest (\ request -> choice router1 (router2 request))
67+choice router1 (Choice router2 router3) = Choice (choice router1 router2) router3
54choice router1 router2 = Choice router1 router2
68choice router1 router2 = Choice router1 router2
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
75+-- Two 'Router's can be structurally compared by computing
76+-- their 'RouterStructure' using 'routerStructure' and
77+-- then testing for equality, see 'sameStructure'.
79+data RouterStructure =
80+ WithRequestStructure RouterStructure
81+ | StaticRouterStructure (Map Text RouterStructure) Int
82+ | DynamicRouterStructure RouterStructure
84+ | ChoiceStructure RouterStructure RouterStructure
87+-- | Compute the structure of a router.
89+-- Assumes that the request or text being passed
90+-- in 'WithRequest' or 'DynamicRouter' does not
91+-- affect the structure of the underlying tree.
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 _) =
104+routerStructure (Choice r1 r2) =
106+ (routerStructure r1)
107+ (routerStructure r2)
109+-- | Compare the structure of two routers.
111+sameStructure :: Router' a -> Router' b -> Bool
112+sameStructure r1 r2 =
113+ routerStructure r1 == routerStructure r2
115+-- | Provide a textual representation of the
116+-- structure of a router.
118+routerLayout :: Router' a -> Text
119+routerLayout router =
120+ T.unlines (["/"] ++ mkRouterLayout False (routerStructure router))
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
131+ mkSubTrees :: Bool -> [(Text, RouterStructure)] -> Int -> [Text]
132+ mkSubTrees _ [] 0 = []
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
140+ mkLeaf :: Bool -> [Text]
141+ mkLeaf True = ["├─•","┆"]
142+ mkLeaf False = ["└─•"]
144+ mkSubTree :: Bool -> Text -> [Text] -> [Text]
145+ mkSubTree True path children = ("├─ " <> path <> "/") : map ("│ " <>) children
146+ mkSubTree False path children = ("└─ " <> path <> "/") : map (" " <>) children
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))
56-- | Interpret a router as an application.
152-- | Interpret a router as an application.
57runRouter :: Router -> RoutingApplication
153runRouter :: Router -> RoutingApplication
58runRouter (WithRequest router) request respond =
154runRouter (WithRequest router) request respond =
59 runRouter (router request) request respond
155 runRouter (router request) request respond
60−runRouter (StaticRouter table) request respond =
156+runRouter (StaticRouter table ls) request respond =
61 case pathInfo request of
157 case pathInfo request of
62− first : rest
158+ [] -> runChoice ls request respond
159+ -- This case is to handle trailing slashes.
160+ [""] -> runChoice ls request respond
63− | Just router <- M.lookup first table
161+ first : rest | Just router <- M.lookup first table
64 -> let request' = request { pathInfo = rest }
162 -> let request' = request { pathInfo = rest }
65 in runRouter router request' respond
163 in runRouter router request' respond
66 _ -> respond $ Fail err404
164 _ -> respond $ Fail err404
67runRouter (DynamicRouter fun) request respond =
165runRouter (DynamicRouter fun) request respond =
68 case pathInfo request of
166 case pathInfo request of
167+ [] -> respond $ Fail err404
168+ -- This case is to handle trailing slashes.
169+ [""] -> respond $ Fail err404
69 first : rest
170 first : rest
70 -> let request' = request { pathInfo = rest }
171 -> let request' = request { pathInfo = rest }
71 in runRouter (fun first) request' respond
172 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
74runRouter (Choice r1 r2) request respond =
174runRouter (Choice r1 r2) request respond =
75− runRouter r1 request $ \ mResponse1 -> case mResponse1 of
76− Fail _ -> runRouter r2 request $ \ mResponse2 ->
175+ runChoice [runRouter r1, runRouter r2] request respond
77− respond (highestPri mResponse1 mResponse2)
78− _ -> respond mResponse1
80− highestPri (Fail e1) (Fail e2) =
81− if worseHTTPCode (errHTTPCode e1) (errHTTPCode e2)
84− highestPri (Fail _) y = y
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.
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 ->
187+ Fail _ -> runChoice rs request $ \ response2 ->
188+ respond $ highestPri response1 response2
189+ _ -> respond response1
191+ highestPri (Fail e1) (Fail e2) =
192+ if worseHTTPCode (errHTTPCode e1) (errHTTPCode e2)
195+ highestPri (Fail _) y = y
88-- Priority on HTTP codes.
198-- Priority on HTTP codes.