simple-0.3.0: Web/Simple/Router.hs
{-# LANGUAGE FlexibleInstances #-}
{- |
Conceptually, a route is function that, given an HTTP request, may return
an action (something that would return a response for the client if run).
Routes can be concatenated--where each route is evaluated until one
matches--and nested. Routes are expressed through the 'Routeable' type class.
'runRoute' transforms an instance of 'Routeable' to a function from 'Request'
to a monadic action (in the 'ResourceT' monad) that returns a
'Maybe' 'Response'. The return type was chosen to be monadic so routing
decisions can depend on side-effects (e.g. a random number or counter for A/B
testing, IP geolocation lookup etc').
-}
module Web.Simple.Router
(
-- * Example
-- $Example
Routeable(..)
, mkRouter
-- * Route Monad
, Route(..)
-- * Common Routes
, routeAll, routeHost, routeTop, routeMethod
, routePattern, routeName, routeVar
) where
import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as S8
import Data.Monoid
import Data.Conduit
import qualified Data.Text as T
import Network.HTTP.Types
import Network.Wai
import Web.Simple.Responses
{- |
'Routeable' types can be converted into a route function using 'runRoute'.
If the route is matched it returns a 'Response', otherwise 'Nothing'.
In general, 'Routeable's are data-dependant (on the 'Request'), but don't have
to be. For example, 'Application' is an instance of 'Routeable' that always
returns a 'Response':
@
instance Routeable Application where
runRoute app req = app req >>= return . Just
@
-}
class Routeable r where
runRoute :: r -> Request -> ResourceT IO (Maybe Response)
-- | Converts any 'Routeable' into an 'Application' that can be passed directly
-- to a WAI server.
mkRouter :: Routeable r => r -> Application
mkRouter route req = do
mapp <- runRoute route req
case mapp of
Just resp -> return resp
Nothing -> return notFound
instance Routeable Application where
runRoute app req = fmap Just $ app req
instance Routeable Response where
runRoute resp = const . return . Just $ resp
{- |
The 'Route' type is a basic instance of 'Routeable' that simply holds the
routing function and an arbitrary additional data parameter. The power is
derived from the instances of 'Monad' and 'Monoid', which allow the
simple construction of complex routing rules using either lists ('Monoid') or
do-notation. Moreover, because of it's simple type, any 'Routeable' can be used
as a 'Route' (using 'routeAll' or by applying it to 'runRoute'), making it
possible to leverage the monadic or monoid syntax for any 'Routeable'.
Commonly, route functions that construct a 'Route' only inspect the 'Request'
and other parameters. For example, 'routeHost' looks at the hostname:
@
routeHost :: Routeable r => S.ByteString -> r -> Route ()
routeHost host route = Route func ()
where func req = if host == serverName req
then runRoute route req
else return Nothing
@
However, because the result of a route is in the
'ResourceT' monad, routes have all the power of an 'Application' and can make
state-dependant decisions. For example, it is trivial to implement a route that
succeeds for every other request (perhaps for A/B testing):
@
routeEveryOther :: (Routeable r1, Routeable r2)
=> TVar Int -> r1 -> r2 -> Route ()
routeEveryOther counter r1 r2 = Route func ()
where func req = do
i <- liftIO . atomically $ do
i' <- readTVar counter
writeTVar counter (i' + 1)
return i'
if i `mod` 2 == 0
then runRoute r1 req
else runRoute r2 req
@
-}
data Route a = Route (Request -> ResourceT IO (Maybe Response)) a
mroute :: (Request -> ResourceT IO (Maybe Response)) -> Route ()
mroute handler = Route handler ()
instance Monad Route where
return a = Route (const $ return Nothing) a
(Route rtA valA) >>= fn =
let (Route rtB valB) = fn valA
in Route (\req -> do
resA <- rtA req
case resA of
Nothing -> rtB req
Just _ -> return resA) valB
instance Monoid (Route ()) where
mempty = mroute $ const $ return Nothing
mappend (Route a _) (Route b _) = mroute $ \req -> do
c <- a req
case c of
Nothing -> b req
Just _ -> return c
instance Routeable (Route a) where
runRoute (Route rtr _) req = rtr req
-- | A route that always matches (useful for converting a 'Routeable' into a
-- 'Route').
routeAll :: Routeable r => r -> Route ()
routeAll = mroute . runRoute
-- | Matches on the hostname from the 'Request'. The route only successeds on
-- exact matches.
routeHost :: Routeable r => S.ByteString -> r -> Route ()
routeHost host route = mroute $ \req ->
if host == serverName req then runRoute route req
else return Nothing
-- | Matches if the path is empty. Note that this route checks that 'pathInfo'
-- is empty, so it works as expected when nested under namespaces or other
-- routes that pop the 'pathInfo' list.
routeTop :: Routeable r => r -> Route ()
routeTop route = mroute $ \req ->
if null (pathInfo req) || (T.length . head $ pathInfo req) == 0
then runRoute route req
else return Nothing
-- | Matches on the HTTP request method (e.g. 'GET', 'POST', 'PUT')
routeMethod :: Routeable r => StdMethod -> r -> Route ()
routeMethod method route = mroute $ \req ->
if renderStdMethod method == requestMethod req then
runRoute route req
else return Nothing
-- | Routes the given URL pattern. Patterns can include
-- directories as well as variable patterns (prefixed with @:@) to be added
-- to 'queryString' (see 'routeVar')
--
-- * \/posts\/:id
--
-- * \/posts\/:id\/new
--
-- * \/:date\/posts\/:category\/new
--
routePattern :: Routeable r => S.ByteString -> r -> Route ()
routePattern pattern route =
let patternParts = map T.unpack $ decodePathSegments pattern
in foldr mkRoute (mroute . runRoute $ routeTop route) patternParts
where mkRoute (':':varName) = routeVar (S8.pack varName)
mkRoute varName = routeName (S8.pack varName)
-- | Matches if the first directory in the path matches the given 'ByteString'
routeName :: Routeable r => S.ByteString -> r -> Route ()
routeName name route = mroute $ \req ->
let poppedHdrReq = req { pathInfo = (tail . pathInfo $ req) }
in if (length $ pathInfo req) > 0 && S8.unpack name == (T.unpack . head . pathInfo) req
then runRoute route poppedHdrReq
else return Nothing
-- | Always matches if there is at least one directory in 'pathInfo' but and
-- adds a parameter to 'queryString' where the key is the first parameter and
-- the value is the directory consumed from the path.
routeVar :: Routeable r => S.ByteString -> r -> Route ()
routeVar varName route = mroute $ \req ->
let varVal = S8.pack . T.unpack . head . pathInfo $ req
poppedHdrReq = req {
pathInfo = (tail . pathInfo $ req)
, queryString = (varName, Just varVal):(queryString req)}
in if (length $ pathInfo req) > 0 then runRoute route poppedHdrReq
else return Nothing
{- $Example
#example#
The most basic 'Routeable' types are 'Application' and 'Response'. Reaching
either of these types marks a termination in the routing lookup. This module
exposes a monadic type 'Route' which makes it easy to create routing logic
in a DSL-like fashion.
'Route's are concatenated using the '>>' operator (or using do-notation).
In the end, any 'Routeable', including a 'Route' is converted to an
'Application' and passed to the server using 'mkRouter':
@
mainAction :: Application
mainAction req = ...
signinForm :: Application
signinForm req = ...
login :: Application
login req = ...
updateProfile :: Application
updateProfile req = ...
main :: IO ()
main = runSettings defaultSettings $ mkRouter $ do
routeTop mainAction
routeName \"sessions\" $ do
routeMethod GET signinForm
routeMethod POST login
routeMethod PUT $ routePattern \"users/:id\" updateProfile
routeAll $ responseLBS status404 [] \"Are you in the right place?\"
@
-}