module Network.Wai.Routing.Route
( Routes
, App
, Continue
, Meta (..)
, prepare
, route
, continue
, addRoute
, attach
, examine
, get
, Network.Wai.Routing.Route.head
, post
, put
, delete
, trace
, options
, connect
, patch
, Renderer
, renderer
, Tree
, Tree.toList
, Tree.foldTree
, Tree.mapTree
, Tree.Payload
, Tree.path
, Tree.value
) where
import Control.Applicative hiding (Const)
import Control.Monad
import Control.Monad.Trans.State.Strict hiding (get, put)
import Data.ByteString (ByteString)
import Data.Either
import Data.Function
import Data.List hiding (head, delete)
import Data.Maybe (mapMaybe, catMaybes)
import Data.Monoid
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Predicate
import Network.Wai.Predicate.Request
import Network.Wai.Route.Tree (Tree)
import Network.Wai.Routing.Request
import Prelude
import qualified Data.ByteString.Char8 as C
import qualified Data.ByteString.Lazy as Lazy
import qualified Data.List as L
import qualified Network.Wai.Route.Tree as Tree
data Route a m = Route
{ _method :: !Method
, _path :: !ByteString
, _meta :: Maybe a
, _pred :: Pack m
}
data Handler m = Handler
{ _delta :: !Double
, _handler :: m ResponseReceived
}
data Pack m where
Pack :: Predicate RoutingReq Error a
-> (a -> Continue m -> m ResponseReceived)
-> Pack m
type Continue m = Response -> m ResponseReceived
type App m = RoutingReq -> Continue m -> m ResponseReceived
type Renderer = Error -> Maybe (Lazy.ByteString, ResponseHeaders)
data Meta a = Meta
{ routeMethod :: !Method
, routePath :: !ByteString
, routeMeta :: a
}
renderer :: Renderer -> Routes a m ()
renderer f = Routes . modify $ \s -> s { renderfn = f }
defRenderer :: Renderer
defRenderer e =
let r = reason2str <$> reason e
s = source2str <$> source e
m = message2str <$> message e
l = labels2str . map Lazy.fromStrict $ labels e
x = case catMaybes [s, r, l] of
[] -> Nothing
xs -> Just (Lazy.intercalate " " xs)
in plainText <$> maybe x (\y -> (<> (" -- " <> y)) <$> x) m
where
reason2str NotAvailable = "not-available"
reason2str TypeError = "type-error"
source2str s = "'" <> Lazy.fromStrict s <> "'"
message2str s = Lazy.fromStrict s
labels2str [] = Nothing
labels2str xs = Just $ "[" <> Lazy.intercalate "," xs <> "]"
plainText s = (s, [(hContentType, "text/plain")])
data St a m = St
{ routes :: [Route a m]
, renderfn :: Renderer
}
zero :: St a m
zero = St [] defRenderer
newtype Routes a m b = Routes { _unroutes :: State (St a m) b }
instance Functor (Routes a m) where
fmap = liftM
instance Applicative (Routes a m) where
pure = return
(<*>) = ap
instance Monad (Routes a m) where
return = Routes . return
m >>= f = Routes $ _unroutes m >>= _unroutes . f
addRoute :: Monad m
=> Method
-> ByteString
-> (a -> Continue m -> m ResponseReceived)
-> Predicate RoutingReq Error a
-> Routes b m ()
addRoute m r x p = Routes . modify $ \s ->
s { routes = Route m r Nothing (Pack p x) : routes s }
get, head, post, put, delete, trace, options, connect, patch ::
Monad m
=> ByteString
-> (a -> Continue m -> m ResponseReceived)
-> Predicate RoutingReq Error a
-> Routes b m ()
get = addRoute (renderStdMethod GET)
head = addRoute (renderStdMethod HEAD)
post = addRoute (renderStdMethod POST)
put = addRoute (renderStdMethod PUT)
delete = addRoute (renderStdMethod DELETE)
trace = addRoute (renderStdMethod TRACE)
options = addRoute (renderStdMethod OPTIONS)
connect = addRoute (renderStdMethod CONNECT)
patch = addRoute (renderStdMethod PATCH)
attach :: a -> Routes a m ()
attach a = Routes $ modify addToLast
where
addToLast s@(St [] _) = s
addToLast (St (r:rr) f) = St (r { _meta = Just a } : rr) f
examine :: Routes a m b -> [Meta a]
examine (Routes r) = let St rr _ = execState r zero in
mapMaybe (\x -> Meta (_method x) (_path x) <$> _meta x) rr
route :: Monad m => Tree (App m) -> Request -> Continue m -> m ResponseReceived
route tr rq k =
case Tree.lookup tr (Tree.segments $ rawPathInfo rq) of
Just e -> Tree.value e (fromReq (Tree.captured $ Tree.captures e) (fromRequest rq)) k
Nothing -> k notFound
where
notFound = responseLBS status404 [] Lazy.empty
continue :: Monad m => (a -> m Response) -> a -> Continue m -> m ResponseReceived
continue f a k = f a >>= k
prepare :: Monad m => Routes a m b -> Tree (App m)
prepare (Routes rr) =
let s = execState rr zero
r = normalise (routes s)
in Tree.fromList $ map (\g -> (_path (L.head g), select (renderfn s) g)) r
normalise :: [Route a m] -> [[Route a m]]
normalise rr =
let rg = grouped . sorted $ rr
paths = map (namelessPath . L.head) rg
ambig = paths \\ nub paths
in if null ambig then rg else error (ambiguityMessage ambig)
where
sorted :: [Route a m] -> [Route a m]
sorted = sortBy (compare `on` _path)
grouped :: [Route a m] -> [[Route a m]]
grouped = groupBy ((==) `on` _path)
namelessPath :: Route a m -> ByteString
namelessPath =
let fun s = if s /= "" && C.head s == ':' then "<>" else s
in C.intercalate "/" . map fun . C.split '/' . _path
ambiguityMessage a =
"Paths differing only in variable names are not supported.\n" ++
"Problematic paths (with variable positions denoted by <>):\n" ++
show a
select :: forall a m. Monad m => Renderer -> [Route a m] -> App m
select render rr req k = do
let ms = filter ((method req ==) . _method) rr
if null ms
then k $ respond render e405 [(allow, validMethods)]
else evalAll ms
where
evalAll :: [Route a m] -> m ResponseReceived
evalAll rs =
let (n, y) = partitionEithers $ foldl' evalSingle [] rs
in if null y
then k $ respond render (L.head n) []
else closest y
evalSingle :: [Either Error (Handler m)] -> Route a m -> [Either Error (Handler m)]
evalSingle rs r =
case _pred r of
Pack p h -> case p req of
Fail m -> Left m : rs
Okay d v -> Right (Handler d (h v k)) : rs
closest :: [Handler m] -> m ResponseReceived
closest hh = case map _handler . sortBy (compare `on` _delta) $ hh of
[] -> k $ responseBuilder status404 [] mempty
h:_ -> h
validMethods :: ByteString
validMethods = C.intercalate "," $ nub (C.pack . show . _method <$> rr)
allow :: HeaderName
allow = "Allow"
respond :: Renderer -> Error -> ResponseHeaders -> Response
respond f e r = case f e of
Just (b, h) -> responseLBS (status e) (r ++ h) b
Nothing -> responseLBS (status e) r Lazy.empty