wai-routes-0.7.1: src/Network/Wai/Middleware/Routes/Routes.hs
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
{- |
Module : Network.Wai.Middleware.Routes.Routes
Copyright : (c) Anupam Jain 2013
License : MIT (see the file LICENSE)
Maintainer : ajnsit@gmail.com
Stability : experimental
Portability : non-portable (uses ghc extensions)
This package provides typesafe URLs for Wai applications.
-}
module Network.Wai.Middleware.Routes.Routes
( -- * Quasi Quoters
parseRoutes -- | Parse Routes declared inline
, parseRoutesFile -- | Parse routes declared in a file
, parseRoutesNoCheck -- | Parse routes declared inline, without checking for overlaps
, parseRoutesFileNoCheck -- | Parse routes declared in a file, without checking for overlaps
-- * Template Haskell methods
, mkRoute
, mkRouteData
, mkRouteDispatch
, mkRouteSubDispatch
-- * Dispatch
, routeDispatch
-- * URL rendering and parsing
, showRoute
, showRouteQuery
, readRoute
-- * Application Handlers
, Handler
, HandlerS
-- * As of Wai 3, Application datatype now follows continuation passing style
-- A `ResponseHandler` represents a continuation passed to the application
, ResponseHandler
-- * Generated Datatypes
, Routable(..) -- | Used internally. However needs to be exported for TH to work.
, RenderRoute(..) -- | A `RenderRoute` instance for your site datatype is automatically generated by `mkRoute`
, ParseRoute(..) -- | A `ParseRoute` instance for your site datatype is automatically generated by `mkRoute`
, RouteAttrs(..) -- | A `RouteAttrs` instance for your site datatype is automatically generated by `mkRoute`
-- * Accessing Request Data
, Env(..)
, RequestData -- | An abstract representation of the request data. You can get the wai request object by using `waiReq`
, waiReq -- | Extract the wai `Request` object from `RequestData`
, nextApp -- | Extract the next Application in the stack
, currentRoute -- | Extract the current `Route` from `RequestData`
, runNext -- | Run the next application in the stack
)
where
-- Wai
import Network.Wai (ResponseReceived, Middleware, Application, pathInfo, requestMethod, requestMethod, Response, Request(..))
import Network.HTTP.Types (decodePath, encodePath, queryTextToQuery, queryToQueryText)
-- Network.Wai.Middleware.Routes
import Network.Wai.Middleware.Routes.Class (Route, RenderRoute(..), ParseRoute(..), RouteAttrs(..))
import Network.Wai.Middleware.Routes.Parse (parseRoutes, parseRoutesNoCheck, parseRoutesFile, parseRoutesFileNoCheck, parseType)
import Network.Wai.Middleware.Routes.TH (mkRenderRouteInstance, mkParseRouteInstance, mkRouteAttrsInstance, mkDispatchClause, ResourceTree(..), MkDispatchSettings(..), defaultGetHandler)
-- Text and Bytestring
import Data.ByteString (ByteString)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
import Blaze.ByteString.Builder (toByteString)
-- TH
import Language.Haskell.TH.Syntax
-- Convenience
import Control.Arrow (second)
import Data.Maybe (fromMaybe)
-- An abstract request
data RequestData master = RequestData
{ waiReq :: Request
, nextApp :: Application
, currentRoute :: Maybe (Route master)
}
-- AJ: Experimental
type ResponseHandler = (Response -> IO ResponseReceived) -> IO ResponseReceived
-- Wai uses Application :: Wai.Request -> ResponseHandler
-- However, instead of Request, we use RequestData which has more information
type App master = RequestData master -> ResponseHandler
data Env sub master = Env
{ envMaster :: master
, envSub :: sub
, envToMaster :: Route sub -> Route master
}
-- | Run the next application in the stack
runNext :: App master
runNext req = nextApp req $ waiReq req
-- | A `Handler` generates an App from the master datatype
type Handler master = HandlerS master master
type HandlerS sub master = Env sub master -> App sub
-- | Generates everything except `Routable` instance and dispatch function
mkRouteData :: String -> [ResourceTree String] -> Q [Dec]
mkRouteData typName routes = do
let typ = parseType typName
let rname = mkName $ "resources" ++ typName
let resourceTrees = map (fmap parseType) routes
eres <- lift routes
let resourcesDec =
[ SigD rname $ ListT `AppT` (ConT ''ResourceTree `AppT` ConT ''String)
, FunD rname [Clause [] (NormalB eres) []]
]
rinst <- mkRenderRouteInstance typ resourceTrees
pinst <- mkParseRouteInstance typ resourceTrees
ainst <- mkRouteAttrsInstance typ resourceTrees
return $ concat [ [ainst]
, [pinst]
, resourcesDec
, rinst
]
mkRouteSubDispatch :: [ResourceTree a] -> Q Exp
mkRouteSubDispatch routes = do
disp <- mkDispatchClause MkDispatchSettings
{ mdsRunHandler = [| runHandler |]
, mdsSubDispatcher = [| subDispatcher |]
, mdsGetPathInfo = [| getPathInfo |]
, mdsMethod = [| getReqMethod |]
, mdsSetPathInfo = [| setPathInfo |]
, mds404 = [| app404 |]
, mds405 = [| app405 |]
, mdsGetHandler = defaultGetHandler
} routes
inner <- newName "inner"
let innerFun = FunD inner [disp]
helper <- newName "helper"
let fun = FunD helper
[ Clause
[]
(NormalB $ VarE inner)
[innerFun]
]
return $ LetE [fun] (VarE helper)
-- | Generates a 'Routable' instance and dispatch function
mkRouteDispatch :: String -> [ResourceTree String] -> Q [Dec]
mkRouteDispatch typName routes = do
let typ = parseType typName
disp <- mkDispatchClause MkDispatchSettings
{ mdsRunHandler = [| runHandler |]
, mdsSubDispatcher = [| subDispatcher |]
, mdsGetPathInfo = [| getPathInfo |]
, mdsMethod = [| getReqMethod |]
, mdsSetPathInfo = [| setPathInfo |]
, mds404 = [| app404 |]
, mds405 = [| app405 |]
, mdsGetHandler = defaultGetHandler
} routes
return [InstanceD []
(ConT ''Routable `AppT` typ `AppT` typ)
[FunD (mkName "dispatcher") [disp]]]
-- | Generates all the things needed for efficient routing,
-- including your application's `Route` datatype, and
-- `RenderRoute`, `ParseRoute`, and `RouteAttr` instances, and
-- `Routable` instance
mkRoute :: String -> [ResourceTree String] -> Q [Dec]
mkRoute typName routes = do
dat <- mkRouteData typName routes
disp <- mkRouteDispatch typName routes
return (disp++dat)
-- | A `Routable` instance can be used in dispatching.
-- An appropriate instance for your site datatype is
-- automatically generated by `mkRoute`.
class Routable sub master where
dispatcher :: HandlerS sub master
-- | Generates the application middleware from a `Routable` master datatype
routeDispatch :: Routable master master => master -> Middleware
-- Env master master is converted to Env sub master by subDispatcher
-- Route information is filled in by runHandler
routeDispatch master def req = dispatcher (_masterToEnv master) RequestData{waiReq=req, nextApp=def, currentRoute=Nothing}
-- | Render a `Route` and Query parameters to Text
showRouteQuery :: RenderRoute master => Route master -> [(Text,Text)] -> Text
showRouteQuery r q = uncurry _encodePathInfo $ second (map (second Just) . (++ q)) $ renderRoute r
-- | Renders a `Route` as Text
showRoute :: RenderRoute master => Route master -> Text
showRoute = uncurry _encodePathInfo . second (map $ second Just) . renderRoute
_encodePathInfo :: [Text] -> [(Text, Maybe Text)] -> Text
-- Slightly hackish: Convert "" into "/"
_encodePathInfo [] = _encodePathInfo [""]
_encodePathInfo segments = decodeUtf8 . toByteString . encodePath segments . queryTextToQuery
-- | Read a route from Text
-- Returns Nothing if Route reading failed. Just route otherwise
readRoute :: ParseRoute master => Text -> Maybe (Route master)
readRoute = parseRoute . second (map (second (fromMaybe "")) . queryToQueryText) . decodePath . encodeUtf8
-- PRIVATE
-- Get the request method from a RequestData
getReqMethod :: RequestData master -> ByteString
getReqMethod = requestMethod . waiReq
-- Get the path info from a RequestData
getPathInfo :: RequestData master -> [Text]
getPathInfo = pathInfo . waiReq
-- Set the path info in a RequestData
setPathInfo :: [Text] -> RequestData master -> RequestData master
setPathInfo p reqData = reqData { waiReq = (waiReq reqData){pathInfo=p} }
-- Baked in applications that handle 404 and 405 errors
-- On no matching route, skip to next application
app404 :: HandlerS sub master
app404 _master = runNext
-- On matching route, but no matching http method, skip to next application
-- This allows a later route to handle methods not implemented by the previous routes
app405 :: HandlerS sub master
app405 _master = runNext
-- Run a route handler function
runHandler
:: HandlerS sub master
-> Env sub master
-> Maybe (Route sub)
-> App sub
runHandler h env route reqdata = h env reqdata{currentRoute=route}
-- Run a route subsite handler function
subDispatcher
:: Routable sub master
=> (HandlerS sub master -> Env sub master -> Maybe (Route sub) -> App sub)
-> (master -> sub)
-> (Route sub -> Route master)
-> Env master master
-> App master
subDispatcher _runhandler getSub toMasterRoute env reqData = dispatcher env' reqData'
where
env' = _envToSub getSub toMasterRoute env
reqData' = reqData{currentRoute=Nothing}
qq (k,mv) = (decodeUtf8 k, maybe "" decodeUtf8 mv)
req = waiReq reqData
_masterToEnv :: master -> Env master master
_masterToEnv master = Env master master id
_envToSub :: (master -> sub) -> (Route sub -> Route master) -> Env master master -> Env sub master
_envToSub getSub toMasterRoute env = Env master sub toMasterRoute
where
master = envMaster env
sub = getSub master