happstack-static-routing-0.7.0.0: src/Happstack/StaticRouting/Internal.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_HADDOCK hide #-}
module Happstack.StaticRouting.Internal where
import Control.Arrow (first)
import Control.Monad (liftM, mplus)
import Data.Map (Map)
import Data.Maybe
import Happstack.Server
( FromReqURI, Method, ServerMonad, askRq, fromReqURI, localRq, rqMethod, rqPaths
)
import qualified Data.ListTrie.Map as Trie
import qualified Data.Map as Map
-- | Static routing tables consisting of handlers of type 'a'.
data Route a
= Dir Segment (Route a)
| Param (Route a)
| Handler EndSegment CheckApply a
| Choice [Route a]
deriving Functor
data Segment = StringS String | ParamS
deriving (Show, Eq, Ord)
type EndSegment = (Maybe Int, Method)
type CheckApply = [String] -> Bool
-- | Support for varying number of arguments to 'path' handlers.
class Path m hm h r | h -> m r where
pathHandler :: forall r'. (m r -> hm r') -> h -> hm r'
arity :: h -> Int
canBeApplied :: h -> [String] -> Bool
instance
( FromReqURI v
, ServerMonad hm
, Path m hm h r
) => Path m hm (v -> h) r where
pathHandler trans f = applyPath (pathHandler trans . f)
arity f = 1 + arity @m @hm (f undefined)
canBeApplied _ [] = False
canBeApplied f (s:ss) = case (fromReqURI s) of
Just p -> canBeApplied @m @hm (f p) ss
Nothing -> False
-- | Pop a path element and parse it using the 'fromReqURI' in the
-- 'FromReqURI' class. Variant of Happstack.Server.path without 'mzero'
applyPath :: (FromReqURI a, ServerMonad m) => (a -> m b) -> m b
applyPath handle = do
rq <- askRq
case rqPaths rq of
(p:xs) | Just a <- fromReqURI p
-> localRq (\newRq -> newRq{rqPaths = xs}) (handle a)
_ -> error "Happstack.StaticRouting.applyPath"
instance Path m hm (m r) r where
pathHandler trans mr = trans mr
arity _ = 0
canBeApplied _ _ = True
-- | Pop a path element if it matches the given string.
dir :: String -> Route a -> Route a
dir = Dir . StringS
-- | Pop a path element, and store it to use with handler
param :: Route a -> Route a
param = Param
-- | Combine several route alternatives into one.
choice :: [Route a] -> Route a
choice = Choice
-- | Expect the given method, and exactly 'n' more segments, where 'n' is the arity of the handler.
path :: forall m hm h r r'. Path m hm h r
=> Method -> (m r -> hm r') -> h -> Route (hm r')
path m trans h = Handler (Just (arity @m @hm h), m) (canBeApplied @m @hm h) (pathHandler trans h)
-- | Expect zero or more segments.
remainingPath :: Method -> h -> Route h
remainingPath m = Handler (Nothing,m) (\_ -> True)
newtype RouteTree a =
R { unR :: Trie.TrieMap Map Segment (Map EndSegment a) } deriving (Show, Functor)
type Segments = ([Segment], EndSegment)
-- | Compile a route into a 'RouteTree'. Turn overlapping routes into 'Nothing'
routeTreeWithOverlaps :: Route a -> RouteTree (Maybe (CheckApply,a))
routeTreeWithOverlaps r =
R $ foldr (\((ps,es),m) ->
Trie.insertWith (Map.unionWith merge)
ps
(Map.singleton es (Just m)))
Trie.empty
(flatten r)
where merge (Just _) _ = Nothing -- overlap
merge Nothing m = m
-- | Check for overlaps in a 'RouteTree', returning either an error
-- message in case of an overlap, or a 'RouteTree' without overlaps.
routeTree :: RouteTree (Maybe (CheckApply, a)) -> Either String (RouteTree (CheckApply, a))
routeTree t | not $ null os =
Left $ unlines $
"Happstack.StaticRouting: Overlapping handlers in" :
map ((" "++) . showSegments) os
| not $ null is =
Left $ unlines $
"Happstack.StaticRouting: Unreachable handler due to ignored parameter in" :
map ((" "++) . showSegments) is
| otherwise = Right $ fmap fromJust t
where os = [ (ss, es) | (ss, m) <- Trie.toList (unR t)
, (es, Nothing) <- Map.toList m
]
is = [ (ss, es) | (ss, m) <- Trie.toList (unR t)
, (es@(Just p, _), _) <- Map.toList m
, p < length (filter ((==) ParamS) $ ss)
]
showSegments :: Segments -> String
showSegments (ss, es) = concatMap showSegment ss ++ showEndSegment es
where
showSegment :: Segment -> String
showSegment (StringS e) = "dir " ++ show e ++ " $ "
showSegment (ParamS) = "param (used in handler) $ "
showEndSegment :: EndSegment -> String
showEndSegment (Just a, m) = "<handler> -- with method " ++ show m ++ " and arity " ++ show a
showEndSegment (Nothing, m) = "remainingPath $ <handler> -- with method " ++ show m
flatten :: Route a -> [(Segments, (CheckApply, a))]
flatten = f where
f (Dir s r) = map (first (first (s:))) (f r)
f (Param r) = map (first (first (ParamS:))) (f r)
f (Handler e ca a) = [(([], e), (ca, a))]
f (Choice rs) = concatMap f rs
-- | Compile routes or return overlap report. Returns 'Left e' in
-- case of order-dependent overlap between handlers, where 'e'
-- describes the overlap. Returns 'Right h', where h is a compiled
-- handler that returns 'Nothing' in case no matching handler was
-- found, otherwise 'Just response'.
compile :: ServerMonad m => Route (m r) -> Either String (m (Maybe r))
compile r = case routeTree $ routeTreeWithOverlaps r of
Left s -> Left s
Right t -> Right $ dispatch t
-- | Dispatch a request given a route. Give priority to more specific paths.
dispatch :: ServerMonad m => RouteTree (CheckApply, m r) -> m (Maybe r)
dispatch t = do
rq <- askRq
case dispatch' [] (rqMethod rq) (rqPaths rq) t of
Just (rq', h) -> Just `liftM` localRq (\newRq -> newRq{ rqPaths = rq'}) h
Nothing -> return Nothing
-- | Dispatch a request given a method and path. Give priority to more specific paths.
-- 'params' holds path segments that where matched 'ParamS' segment.
dispatch' :: forall a. [String] -> Method -> [String] -> RouteTree (CheckApply, a) -> Maybe ([String], a)
dispatch' params m ps (R t) = dChildren ps `mplus` fmap (params ++ ps,) dNode
where
-- most specific: look up a segment in the children and recurse
dChildren :: [String] -> Maybe ([String], a)
dChildren (p:ps') = ((Map.lookup (StringS p) (Trie.children1 t)) >>= dispatch' params m ps' . R)
`mplus` ((Map.lookup (ParamS) (Trie.children1 t)) >>= dispatch' (params ++ [p]) m ps' . R)
dChildren [] = Nothing
dNode :: Maybe a
dNode = do
-- Find a handler that does not need any more segments
em <- Trie.lookup [] t
-- Select one that matches number of parameters or one that will ignore them (created with 'remainingPath')
(ac,h) <- (Map.lookup (Just (length ps + length params), m) em) `mplus` (Map.lookup (Nothing, m) em)
-- Make sure that params can converted with fromReqURI
if (ac (params ++ ps))
then return h
else Nothing