2015-10-12 18:51:43 +02:00
|
|
|
{-# LANGUAGE DeriveFunctor #-}
|
2015-09-15 11:37:17 +02:00
|
|
|
{-# LANGUAGE CPP #-}
|
2016-04-09 15:42:57 +02:00
|
|
|
{-# LANGUAGE GADTs #-}
|
|
|
|
{-# LANGUAGE KindSignatures #-}
|
2016-04-07 13:45:15 +02:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2015-06-01 19:38:51 +02:00
|
|
|
module Servant.Server.Internal.Router where
|
|
|
|
|
2015-08-17 23:56:29 +02:00
|
|
|
import Data.Map (Map)
|
|
|
|
import qualified Data.Map as M
|
2016-04-07 13:45:15 +02:00
|
|
|
import Data.Monoid
|
2015-08-17 23:56:29 +02:00
|
|
|
import Data.Text (Text)
|
2016-04-07 13:45:15 +02:00
|
|
|
import qualified Data.Text as T
|
2016-04-09 15:42:57 +02:00
|
|
|
import Network.Wai (Response, pathInfo)
|
2015-06-01 19:38:51 +02:00
|
|
|
import Servant.Server.Internal.RoutingApplication
|
2015-09-16 22:07:55 +02:00
|
|
|
import Servant.Server.Internal.ServantErr
|
2015-06-01 19:38:51 +02:00
|
|
|
|
2016-04-09 15:42:57 +02:00
|
|
|
type Router env = Router' env RoutingApplication
|
2015-10-12 18:51:43 +02:00
|
|
|
|
2015-06-01 19:38:51 +02:00
|
|
|
-- | Internal representation of a router.
|
2016-04-09 15:42:57 +02:00
|
|
|
--
|
|
|
|
-- The first argument describes an environment type that is
|
|
|
|
-- expected as extra input by the routers at the leaves. The
|
|
|
|
-- environment is filled while running the router, with path
|
|
|
|
-- components that can be used to process captures.
|
|
|
|
--
|
|
|
|
data Router' env a =
|
|
|
|
StaticRouter (Map Text (Router' env a)) [env -> a]
|
2016-04-07 13:45:15 +02:00
|
|
|
-- ^ the map contains routers for subpaths (first path component used
|
|
|
|
-- for lookup and removed afterwards), the list contains handlers
|
|
|
|
-- for the empty path, to be tried in order
|
2016-04-09 15:42:57 +02:00
|
|
|
| CaptureRouter (Router' (Text, env) a)
|
|
|
|
-- ^ first path component is passed to the child router in its
|
|
|
|
-- environment and removed afterwards
|
|
|
|
| RawRouter (env -> a)
|
2016-04-07 13:45:15 +02:00
|
|
|
-- ^ to be used for routes we do not know anything about
|
2016-04-09 15:42:57 +02:00
|
|
|
| Choice (Router' env a) (Router' env a)
|
2015-06-01 19:38:51 +02:00
|
|
|
-- ^ left-biased choice between two routers
|
2015-10-12 18:51:43 +02:00
|
|
|
deriving Functor
|
2015-06-01 19:38:51 +02:00
|
|
|
|
2016-04-07 13:45:15 +02:00
|
|
|
-- | Smart constructor for a single static path component.
|
2016-04-09 15:42:57 +02:00
|
|
|
pathRouter :: Text -> Router' env a -> Router' env a
|
2016-04-07 13:45:15 +02:00
|
|
|
pathRouter t r = StaticRouter (M.singleton t r) []
|
|
|
|
|
|
|
|
-- | Smart constructor for a leaf, i.e., a router that expects
|
|
|
|
-- the empty path.
|
|
|
|
--
|
2016-04-09 15:42:57 +02:00
|
|
|
leafRouter :: (env -> a) -> Router' env a
|
2016-04-07 13:45:15 +02:00
|
|
|
leafRouter l = StaticRouter M.empty [l]
|
2015-10-12 17:06:43 +02:00
|
|
|
|
2015-06-01 19:38:51 +02:00
|
|
|
-- | Smart constructor for the choice between routers.
|
|
|
|
-- We currently optimize the following cases:
|
|
|
|
--
|
2016-04-07 13:45:15 +02:00
|
|
|
-- * Two static routers can be joined by joining their maps
|
|
|
|
-- and concatenating their leaf-lists.
|
2015-06-01 19:38:51 +02:00
|
|
|
-- * Two dynamic routers can be joined by joining their codomains.
|
2016-04-07 13:45:15 +02:00
|
|
|
-- * Choice nodes can be reordered.
|
2015-06-01 19:38:51 +02:00
|
|
|
--
|
2016-04-09 15:42:57 +02:00
|
|
|
choice :: Router' env a -> Router' env a -> Router' env a
|
2016-04-07 13:45:15 +02:00
|
|
|
choice (StaticRouter table1 ls1) (StaticRouter table2 ls2) =
|
|
|
|
StaticRouter (M.unionWith choice table1 table2) (ls1 ++ ls2)
|
2016-04-09 15:42:57 +02:00
|
|
|
choice (CaptureRouter router1) (CaptureRouter router2) =
|
|
|
|
CaptureRouter (choice router1 router2)
|
2016-04-07 13:45:15 +02:00
|
|
|
choice router1 (Choice router2 router3) = Choice (choice router1 router2) router3
|
2015-06-01 19:38:51 +02:00
|
|
|
choice router1 router2 = Choice router1 router2
|
|
|
|
|
2016-04-07 13:45:15 +02:00
|
|
|
-- | Datatype used for representing and debugging the
|
2016-04-09 15:42:57 +02:00
|
|
|
-- structure of a router. Abstracts from the handlers
|
|
|
|
-- at the leaves.
|
2016-04-07 13:45:15 +02:00
|
|
|
--
|
|
|
|
-- Two 'Router's can be structurally compared by computing
|
|
|
|
-- their 'RouterStructure' using 'routerStructure' and
|
|
|
|
-- then testing for equality, see 'sameStructure'.
|
|
|
|
--
|
|
|
|
data RouterStructure =
|
2016-04-09 15:42:57 +02:00
|
|
|
StaticRouterStructure (Map Text RouterStructure) Int
|
|
|
|
| CaptureRouterStructure RouterStructure
|
2016-04-07 13:45:15 +02:00
|
|
|
| RawRouterStructure
|
|
|
|
| ChoiceStructure RouterStructure RouterStructure
|
|
|
|
deriving (Eq, Show)
|
|
|
|
|
|
|
|
-- | Compute the structure of a router.
|
|
|
|
--
|
|
|
|
-- Assumes that the request or text being passed
|
2016-04-09 15:42:57 +02:00
|
|
|
-- in 'WithRequest' or 'CaptureRouter' does not
|
2016-04-07 13:45:15 +02:00
|
|
|
-- affect the structure of the underlying tree.
|
|
|
|
--
|
2016-04-09 15:42:57 +02:00
|
|
|
routerStructure :: Router' env a -> RouterStructure
|
2016-04-07 13:45:15 +02:00
|
|
|
routerStructure (StaticRouter m ls) =
|
|
|
|
StaticRouterStructure (fmap routerStructure m) (length ls)
|
2016-04-09 15:42:57 +02:00
|
|
|
routerStructure (CaptureRouter router) =
|
|
|
|
CaptureRouterStructure $
|
|
|
|
routerStructure router
|
2016-04-07 13:45:15 +02:00
|
|
|
routerStructure (RawRouter _) =
|
|
|
|
RawRouterStructure
|
|
|
|
routerStructure (Choice r1 r2) =
|
|
|
|
ChoiceStructure
|
|
|
|
(routerStructure r1)
|
|
|
|
(routerStructure r2)
|
|
|
|
|
|
|
|
-- | Compare the structure of two routers.
|
|
|
|
--
|
2016-04-09 15:42:57 +02:00
|
|
|
sameStructure :: Router' env a -> Router' env b -> Bool
|
2016-04-07 13:45:15 +02:00
|
|
|
sameStructure r1 r2 =
|
|
|
|
routerStructure r1 == routerStructure r2
|
|
|
|
|
|
|
|
-- | Provide a textual representation of the
|
|
|
|
-- structure of a router.
|
|
|
|
--
|
2016-04-09 15:42:57 +02:00
|
|
|
routerLayout :: Router' env a -> Text
|
2016-04-07 13:45:15 +02:00
|
|
|
routerLayout router =
|
|
|
|
T.unlines (["/"] ++ mkRouterLayout False (routerStructure router))
|
|
|
|
where
|
|
|
|
mkRouterLayout :: Bool -> RouterStructure -> [Text]
|
|
|
|
mkRouterLayout c (StaticRouterStructure m n) = mkSubTrees c (M.toList m) n
|
2016-04-09 15:42:57 +02:00
|
|
|
mkRouterLayout c (CaptureRouterStructure r) = mkSubTree c "<capture>" (mkRouterLayout False r)
|
2016-04-07 13:45:15 +02:00
|
|
|
mkRouterLayout c RawRouterStructure =
|
|
|
|
if c then ["├─ <raw>"] else ["└─ <raw>"]
|
|
|
|
mkRouterLayout c (ChoiceStructure r1 r2) =
|
|
|
|
mkRouterLayout True r1 ++ ["┆"] ++ mkRouterLayout c r2
|
|
|
|
|
|
|
|
mkSubTrees :: Bool -> [(Text, RouterStructure)] -> Int -> [Text]
|
|
|
|
mkSubTrees _ [] 0 = []
|
|
|
|
mkSubTrees c [] n =
|
|
|
|
concat (replicate (n - 1) (mkLeaf True) ++ [mkLeaf c])
|
|
|
|
mkSubTrees c [(t, r)] 0 =
|
|
|
|
mkSubTree c t (mkRouterLayout False r)
|
|
|
|
mkSubTrees c ((t, r) : trs) n =
|
|
|
|
mkSubTree True t (mkRouterLayout False r) ++ mkSubTrees c trs n
|
|
|
|
|
|
|
|
mkLeaf :: Bool -> [Text]
|
|
|
|
mkLeaf True = ["├─•","┆"]
|
|
|
|
mkLeaf False = ["└─•"]
|
|
|
|
|
|
|
|
mkSubTree :: Bool -> Text -> [Text] -> [Text]
|
|
|
|
mkSubTree True path children = ("├─ " <> path <> "/") : map ("│ " <>) children
|
|
|
|
mkSubTree False path children = ("└─ " <> path <> "/") : map (" " <>) children
|
|
|
|
|
|
|
|
-- | Apply a transformation to the response of a `Router`.
|
2016-04-09 15:42:57 +02:00
|
|
|
tweakResponse :: (RouteResult Response -> RouteResult Response) -> Router env -> Router env
|
2016-04-07 13:45:15 +02:00
|
|
|
tweakResponse f = fmap (\a -> \req cont -> a req (cont . f))
|
|
|
|
|
2015-06-01 19:38:51 +02:00
|
|
|
-- | Interpret a router as an application.
|
2016-04-09 15:42:57 +02:00
|
|
|
runRouter :: Router () -> RoutingApplication
|
|
|
|
runRouter r = runRouterEnv r ()
|
|
|
|
|
|
|
|
runRouterEnv :: Router env -> env -> RoutingApplication
|
|
|
|
runRouterEnv router env request respond =
|
|
|
|
case router of
|
|
|
|
StaticRouter table ls ->
|
|
|
|
case pathInfo request of
|
|
|
|
[] -> runChoice ls env request respond
|
|
|
|
-- This case is to handle trailing slashes.
|
|
|
|
[""] -> runChoice ls env request respond
|
|
|
|
first : rest | Just router' <- M.lookup first table
|
|
|
|
-> let request' = request { pathInfo = rest }
|
|
|
|
in runRouterEnv router' env request' respond
|
|
|
|
_ -> respond $ Fail err404
|
|
|
|
CaptureRouter router' ->
|
|
|
|
case pathInfo request of
|
|
|
|
[] -> respond $ Fail err404
|
|
|
|
-- This case is to handle trailing slashes.
|
|
|
|
[""] -> respond $ Fail err404
|
|
|
|
first : rest
|
|
|
|
-> let request' = request { pathInfo = rest }
|
|
|
|
in runRouterEnv router' (first, env) request' respond
|
|
|
|
RawRouter app ->
|
|
|
|
app env request respond
|
|
|
|
Choice r1 r2 ->
|
|
|
|
runChoice [runRouterEnv r1, runRouterEnv r2] env request respond
|
2015-09-16 22:07:55 +02:00
|
|
|
|
2016-04-07 13:45:15 +02:00
|
|
|
-- | Try a list of routing applications in order.
|
|
|
|
-- We stop as soon as one fails fatally or succeeds.
|
|
|
|
-- If all fail normally, we pick the "best" error.
|
|
|
|
--
|
2016-04-09 15:42:57 +02:00
|
|
|
runChoice :: [env -> RoutingApplication] -> env -> RoutingApplication
|
|
|
|
runChoice ls =
|
|
|
|
case ls of
|
|
|
|
[] -> \ _ _ respond -> respond (Fail err404)
|
|
|
|
[r] -> r
|
|
|
|
(r : rs) ->
|
|
|
|
\ env request respond ->
|
|
|
|
r env request $ \ response1 ->
|
|
|
|
case response1 of
|
|
|
|
Fail _ -> runChoice rs env request $ \ response2 ->
|
|
|
|
respond $ highestPri response1 response2
|
|
|
|
_ -> respond response1
|
2016-04-07 13:45:15 +02:00
|
|
|
where
|
|
|
|
highestPri (Fail e1) (Fail e2) =
|
|
|
|
if worseHTTPCode (errHTTPCode e1) (errHTTPCode e2)
|
|
|
|
then Fail e2
|
|
|
|
else Fail e1
|
|
|
|
highestPri (Fail _) y = y
|
|
|
|
highestPri x _ = x
|
2015-09-16 22:07:55 +02:00
|
|
|
|
|
|
|
-- Priority on HTTP codes.
|
|
|
|
--
|
|
|
|
-- It just so happens that 404 < 405 < 406 as far as
|
|
|
|
-- we are concerned here, so we can use (<).
|
|
|
|
worseHTTPCode :: Int -> Int -> Bool
|
|
|
|
worseHTTPCode = (<)
|