module Snap.Internal.Routing where ------------------------------------------------------------------------------ import Control.Applicative ((<|>)) import Data.ByteString (ByteString) import Data.ByteString.Internal (c2w) import qualified Data.ByteString as B import Data.Monoid import qualified Data.Map as Map ------------------------------------------------------------------------------ import Snap.Internal.Http.Types import Snap.Internal.Types ------------------------------------------------------------------------------ {-| The internal data type you use to build a routing tree. Matching is done unambiguously. 'Capture' and 'Dir' routes can have a "fallback" route: - For 'Capture', the fallback is routed when there is nothing to capture - For 'Dir', the fallback is routed when we can't find a route in its map Fallback routes are stacked: i.e. for a route like: > Dir [("foo", Capture "bar" (Action bar) NoRoute)] baz visiting the URI foo/ will result in the "bar" capture being empty and triggering its fallback. It's NoRoute, so we go to the nearest parent fallback and try that, which is the baz action. -} data Route a = Action (Snap a) -- wraps a 'Snap' action | Capture ByteString (Route a) (Route a) -- captures the dir in a param | Dir (Map.Map ByteString (Route a)) (Route a) -- match on a dir | NoRoute ------------------------------------------------------------------------------ instance Monoid (Route a) where mempty = NoRoute -- Unions two routes, favoring the right-hand side mappend NoRoute r = r mappend l@(Action _) r = case r of (Action _) -> r (Capture p r' fb) -> Capture p r' (mappend fb l) (Dir _ _) -> mappend (Dir Map.empty l) r NoRoute -> l mappend l@(Capture p r' fb) r = case r of (Action _) -> Capture p r' (mappend fb r) (Capture p' r'' fb') | p == p' -> Capture p (mappend r' r'') (mappend fb fb') | otherwise -> r (Dir rm fb') -> Dir rm (mappend fb' l) NoRoute -> l mappend l@(Dir rm fb) r = case r of (Action _) -> Dir rm (mappend fb r) (Capture _ _ _) -> Dir rm (mappend fb r) (Dir rm' fb') -> Dir (Map.unionWith mappend rm rm') (mappend fb fb') NoRoute -> l ------------------------------------------------------------------------------ -- | A web handler which, given a mapping from URL entry points to web -- handlers, efficiently routes requests to the correct handler. -- -- The URL entry points are given as relative paths, for example: -- -- > route [ ("foo/bar/quux", fooBarQuux) ] -- -- If the URI of the incoming request is -- -- > /foo/bar/quux -- -- or -- -- > /foo/bar/quux/...anything... -- -- then the request will be routed to \"@fooBarQuux@\", with 'rqContextPath' -- set to \"@\/foo\/bar\/quux\/@\" and 'rqPathInfo' set to -- \"@...anything...@\". -- -- @FIXME@\/@TODO@: we need a version with and without the context path setting -- behaviour; if the route is \"@article\/:id\/print@\", we probably want the -- contextPath to be \"@\/article@\" instead of \"@\/article\/whatever\/print@\". -- -- A path component within an URL entry point beginning with a colon (\"@:@\") -- is treated as a /variable capture/; the corresponding path component within -- the request URI will be entered into the 'rqParams' parameters mapping with -- the given name. For instance, if the routes were: -- -- > route [ ("foo/:bar/baz", fooBazHandler) ] -- -- Then a request for \"@\/foo\/saskatchewan\/baz@\" would be routed to -- @fooBazHandler@ with a mapping for: -- -- > "bar" => "saskatchewan" -- -- in its parameters table. -- -- Longer paths are matched first, and specific routes are matched before -- captures. That is, if given routes: -- -- > [ ("a", h1), ("a/b", h2), ("a/:x", h3) ] -- -- a request for \"@\/a\/b@\" will go to @h2@, \"@\/a\/s@\" for any /s/ will go -- to @h3@, and \"@\/a@\" will go to @h1@. -- -- The following example matches \"@\/article@\" to an article index, -- \"@\/login@\" to a login, and \"@\/article\/...@\" to an article renderer. -- -- > route [ ("article", renderIndex) -- > , ("article/:id", renderArticle) -- > , ("login", method POST doLogin) ] -- route :: [(ByteString, Snap a)] -> Snap a route rts = route' (return ()) rts' [] where rts' = mconcat (map pRoute rts) ------------------------------------------------------------------------------ -- | The 'routeLocal' function is the same as 'route', except it doesn't change -- the request's context path. This is useful if you want to route to a -- particular handler but you want that handler to receive the 'rqPathInfo' as -- it is. routeLocal :: [(ByteString, Snap a)] -> Snap a routeLocal rts' = do req <- getRequest let ctx = rqContextPath req let p = rqPathInfo req let md = modifyRequest $ \r -> r {rqContextPath=ctx, rqPathInfo=p} route' md rts [] <|> (md >> pass) where rts = mconcat (map pRoute rts') ------------------------------------------------------------------------------ pRoute :: (ByteString, Snap a) -> Route a pRoute (r, a) = foldr f (Action a) hier where hier = filter (not . B.null) $ B.splitWith (== (c2w '/')) r f s rt = if B.head s == c2w ':' then Capture (B.tail s) rt NoRoute else Dir (Map.fromList [(s, rt)]) NoRoute ------------------------------------------------------------------------------ route' :: Snap () -- ^ an action to be run before any user -- handler -> Route a -- ^ currently active routing table -> [Route a] -- ^ list of fallback routing tables in case -- the current table fails -> Snap a route' pre (Action action) _ = pre >> action route' pre (Capture param rt fb) fbs = do cwd <- getRequest >>= return . B.takeWhile (/= (c2w '/')) . rqPathInfo if B.null cwd then route' pre fb fbs else do localRequest (updateContextPath (B.length cwd) . (f cwd)) $ route' pre rt (fb:fbs) where f v req = req { rqParams = Map.insertWith (++) param [v] (rqParams req) } route' pre (Dir rtm fb) fbs = do cwd <- getRequest >>= return . B.takeWhile (/= (c2w '/')) . rqPathInfo case Map.lookup cwd rtm of Just rt -> do localRequest (updateContextPath (B.length cwd)) $ route' pre rt (fb:fbs) Nothing -> route' pre fb fbs route' _ NoRoute [] = pass route' pre NoRoute (fb:fbs) = route' pre fb fbs