yesod/yesod-core/Yesod/Core/Dispatch.hs
2013-03-13 08:48:28 +02:00

253 lines
9.6 KiB
Haskell

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module Yesod.Core.Dispatch
( -- * Quasi-quoted routing
parseRoutes
, parseRoutesNoCheck
, parseRoutesFile
, parseRoutesFileNoCheck
, mkYesod
, mkYesodSub
-- ** More fine-grained
, mkYesodData
, mkYesodSubData
, mkYesodDispatch
, mkYesodSubDispatch
, mkDispatchInstance
-- ** Path pieces
, PathPiece (..)
, PathMultiPiece (..)
, Texts
-- * Convert to WAI
, toWaiApp
, toWaiAppPlain
-- * WAI subsites
, WaiSubsite (..)
) where
import Control.Applicative ((<$>), (<*>))
import Prelude hiding (exp)
import Yesod.Core.Handler
import Web.PathPieces
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import qualified Network.Wai as W
import Network.Wai.Middleware.Gzip
import Network.Wai.Middleware.Autohead
import Data.ByteString.Lazy.Char8 ()
import Data.Text (Text)
import Data.Monoid (mappend)
import qualified Data.ByteString as S
import qualified Blaze.ByteString.Builder
import Network.HTTP.Types (status301)
import Yesod.Routes.TH
import Yesod.Routes.Parse
import System.Log.FastLogger (Logger)
import Yesod.Core.Types
import Yesod.Core.Content
import Yesod.Core.Class.Yesod
import Yesod.Core.Class.Dispatch
import Yesod.Core.Internal.Run
import Yesod.Core.Class.Handler
-- | Generates URL datatype and site function for the given 'Resource's. This
-- is used for creating sites, /not/ subsites. See 'mkYesodSub' for the latter.
-- Use 'parseRoutes' to create the 'Resource's.
mkYesod :: String -- ^ name of the argument datatype
-> [ResourceTree String]
-> Q [Dec]
mkYesod name = fmap (uncurry (++)) . mkYesodGeneral name [] [] False
-- | Generates URL datatype and site function for the given 'Resource's. This
-- is used for creating subsites, /not/ sites. See 'mkYesod' for the latter.
-- Use 'parseRoutes' to create the 'Resource's. In general, a subsite is not
-- executable by itself, but instead provides functionality to
-- be embedded in other sites.
mkYesodSub :: String -- ^ name of the argument datatype
-> Cxt
-> [ResourceTree String]
-> Q [Dec]
mkYesodSub name clazzes =
fmap (uncurry (++)) . mkYesodGeneral name' rest clazzes True
where
(name':rest) = words name
-- | Sometimes, you will want to declare your routes in one file and define
-- your handlers elsewhere. For example, this is the only way to break up a
-- monolithic file into smaller parts. Use this function, paired with
-- 'mkYesodDispatch', to do just that.
mkYesodData :: String -> [ResourceTree String] -> Q [Dec]
mkYesodData name res = mkYesodDataGeneral name [] False res
mkYesodSubData :: String -> Cxt -> [ResourceTree String] -> Q [Dec]
mkYesodSubData name clazzes res = mkYesodDataGeneral name clazzes True res
mkYesodDataGeneral :: String -> Cxt -> Bool -> [ResourceTree String] -> Q [Dec]
mkYesodDataGeneral name clazzes isSub res = do
let (name':rest) = words name
(x, _) <- mkYesodGeneral name' rest clazzes isSub res
let rname = mkName $ "resources" ++ name
eres <- lift res
let y = [ SigD rname $ ListT `AppT` (ConT ''ResourceTree `AppT` ConT ''String)
, FunD rname [Clause [] (NormalB eres) []]
]
return $ x ++ y
-- | See 'mkYesodData'.
mkYesodDispatch :: String -> [ResourceTree String] -> Q [Dec]
mkYesodDispatch name = fmap snd . mkYesodGeneral name [] [] False
mkYesodSubDispatch :: String -> Cxt -> [ResourceTree String] -> Q [Dec]
mkYesodSubDispatch name clazzes = fmap snd . mkYesodGeneral name' rest clazzes True
where (name':rest) = words name
mkYesodGeneral :: String -- ^ foundation type
-> [String] -- ^ arguments for the type
-> Cxt -- ^ the type constraints
-> Bool -- ^ it this a subsite
-> [ResourceTree String]
-> Q([Dec],[Dec])
mkYesodGeneral name args clazzes isSub resS = do
subsite <- sub
masterTypeSyns <- if isSub then return []
else sequence [handler, widget]
renderRouteDec <- mkRenderRouteInstance subsite res
dispatchDec <- mkDispatchInstance context (if isSub then Just sub else Nothing) master res
return (renderRouteDec ++ masterTypeSyns, dispatchDec)
where sub = foldl appT subCons subArgs
master = if isSub then (varT $ mkName "master") else sub
context = if isSub then cxt $ yesod : map return clazzes
else return []
yesod = classP ''HandlerReader [master]
handler = tySynD (mkName "Handler") [] [t| GHandler $master $master |]
widget = tySynD (mkName "Widget") [] [t| GWidget $master $master () |]
res = map (fmap parseType) resS
subCons = conT $ mkName name
subArgs = map (varT. mkName) args
-- | If the generation of @'YesodDispatch'@ instance require finer
-- control of the types, contexts etc. using this combinator. You will
-- hardly need this generality. However, in certain situations, like
-- when writing library/plugin for yesod, this combinator becomes
-- handy.
mkDispatchInstance :: CxtQ -- ^ The context
-> Maybe TypeQ -- ^ The subsite type
-> TypeQ -- ^ The master site type
-> [ResourceTree a] -- ^ The resource
-> DecsQ
mkDispatchInstance context Nothing master res = do
let yDispatch = conT ''YesodDispatch `appT` master `appT` master
thisDispatch = do
clause' <- mkDispatchClause MkDispatchSettings
{ mdsRunHandler = [|yesodRunner|]
, mdsSubDispatcher = [|yesodSubDispatch|]
, mdsGetPathInfo = [|W.pathInfo|]
, mdsSetPathInfo = [|\p r -> r { W.pathInfo = p }|]
, mdsMethod = [|W.requestMethod|]
, mds404 = [|notFound >> return ()|]
, mds405 = [|badMethod >> return ()|]
} res
return $ FunD 'yesodDispatch [clause']
in sequence [instanceD context yDispatch [thisDispatch]]
mkDispatchInstance context (Just sub) master res = do
yDispatch <- conT ''YesodSubDispatch `appT` sub `appT` master
parentRunner <- newName "parentRunner"
getSub <- newName "getSub"
toMaster <- newName "toMaster"
runner <- newName "runner"
clause' <- mkDispatchClause MkDispatchSettings
{ mdsRunHandler = [|subHelper
$(return $ VarE parentRunner)
$(return $ VarE getSub)
$(return $ VarE toMaster)
. fmap toTypedContent
|]
, mdsSubDispatcher = [|yesodSubDispatch|]
, mdsGetPathInfo = [|W.pathInfo|]
, mdsSetPathInfo = [|\p r -> r { W.pathInfo = p }|]
, mdsMethod = [|W.requestMethod|]
, mds404 = [|notFound >> return ()|]
, mds405 = [|badMethod >> return ()|]
} res
inner <- newName "inner"
err <- [|error "FIXME"|]
let innerFun = FunD inner [clause']
runnerFun = FunD runner
[ Clause
[]
(NormalB $ VarE 'subHelper
`AppE` VarE parentRunner
`AppE` VarE getSub
`AppE` VarE toMaster
)
[]
]
context' <- context
let fun = FunD 'yesodSubDispatch
[ Clause
[VarP parentRunner, VarP getSub, VarP toMaster]
(NormalB $ VarE inner)
[innerFun, runnerFun]
]
return [InstanceD context' yDispatch [fun]]
-- | Convert the given argument into a WAI application, executable with any WAI
-- handler. This is the same as 'toWaiAppPlain', except it includes two
-- middlewares: GZIP compression and autohead. This is the
-- recommended approach for most users.
toWaiApp :: ( Yesod master
, YesodDispatch master master
) => master -> IO W.Application
toWaiApp y = gzip (gzipSettings y) . autohead <$> toWaiAppPlain y
-- | Convert the given argument into a WAI application, executable with any WAI
-- handler. This differs from 'toWaiApp' in that it uses no middlewares.
toWaiAppPlain :: ( Yesod master
, YesodDispatch master master
) => master -> IO W.Application
toWaiAppPlain a = toWaiApp' a <$> getLogger a <*> makeSessionBackend a
toWaiApp' :: ( Yesod master
, YesodDispatch master master
)
=> master
-> Logger
-> Maybe SessionBackend
-> W.Application
toWaiApp' y logger sb req =
case cleanPath y $ W.pathInfo req of
Left pieces -> sendRedirect y pieces req
Right pieces -> yesodDispatch yre req
{ W.pathInfo = pieces
}
where
yre = YesodRunnerEnv
{ yreLogger = logger
, yreMaster = y
, yreSub = y
, yreToMaster = id
, yreSessionBackend = sb
}
sendRedirect :: Yesod master => master -> [Text] -> W.Application
sendRedirect y segments' env =
return $ W.responseLBS status301
[ ("Content-Type", "text/plain")
, ("Location", Blaze.ByteString.Builder.toByteString dest')
] "Redirecting"
where
dest = joinPath y (resolveApproot y env) segments' []
dest' =
if S.null (W.rawQueryString env)
then dest
else (dest `mappend`
Blaze.ByteString.Builder.fromByteString (W.rawQueryString env))