{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} module Yesod.Dispatch ( -- * Quasi-quoted routing parseRoutes , parseRoutesNoCheck , parseRoutesFile , parseRoutesFileNoCheck , mkYesod , mkYesodSub -- ** More fine-grained , mkYesodData , mkYesodSubData , mkYesodDispatch , mkYesodSubDispatch -- ** Path pieces , PathPiece (..) , PathMultiPiece (..) , Texts -- * Convert to WAI , toWaiApp , toWaiAppPlain -- * WAI subsites , WaiSubsite (..) ) where import Data.Functor ((<$>)) import Prelude hiding (exp) import Yesod.Internal.Core import Yesod.Handler hiding (lift) import Yesod.Widget (GWidget) import Web.PathPieces 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.Text.Encoding (decodeUtf8With) import Data.Text.Encoding.Error (lenientDecode) 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.Content (chooseRep) import Yesod.Routes.Parse type Texts = [Text] -- | 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 -> [Resource 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 -> [Resource 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 -> [Resource String] -> Q [Dec] mkYesodData name res = mkYesodDataGeneral name [] False res mkYesodSubData :: String -> Cxt -> [Resource String] -> Q [Dec] mkYesodSubData name clazzes res = mkYesodDataGeneral name clazzes True res mkYesodDataGeneral :: String -> Cxt -> Bool -> [Resource 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 ''Resource `AppT` ConT ''String) , FunD rname [Clause [] (NormalB eres) []] ] return $ x ++ y -- | See 'mkYesodData'. mkYesodDispatch :: String -> [Resource String] -> Q [Dec] mkYesodDispatch name = fmap snd . mkYesodGeneral name [] [] False mkYesodSubDispatch :: String -> Cxt -> [Resource String] -> Q [Dec] mkYesodSubDispatch name clazzes = fmap snd . mkYesodGeneral name' rest clazzes True where (name':rest) = words name mkYesodGeneral :: String -- ^ foundation type -> [String] -> Cxt -- ^ classes -> Bool -- ^ is subsite? -> [Resource String] -> Q ([Dec], [Dec]) mkYesodGeneral name args clazzes isSub resS = do let args' = map mkName args arg = foldl AppT (ConT name') $ map VarT args' let res = map (fmap parseType) resS renderRouteDec <- mkRenderRouteInstance arg res disp <- mkDispatchClause [|yesodRunner|] [|yesodDispatch|] [|fmap chooseRep|] res let master = mkName "master" let ctx = if isSub then ClassP (mkName "Yesod") [VarT master] : clazzes else [] let ytyp = if isSub then ConT ''YesodDispatch `AppT` arg `AppT` VarT master else ConT ''YesodDispatch `AppT` arg `AppT` arg let yesodDispatch' = InstanceD ctx ytyp [FunD (mkName "yesodDispatch") [disp]] return (renderRouteDec : masterTypSyns, [yesodDispatch']) where name' = mkName name masterTypSyns | isSub = [] | otherwise = [ TySynD (mkName "Handler") [] (ConT ''GHandler `AppT` ConT name' `AppT` ConT name') , TySynD (mkName "Widget") [] (ConT ''GWidget `AppT` ConT name' `AppT` ConT name' `AppT` TupleT 0) ] -- | 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 <$> makeSessionBackend a toWaiApp' :: ( Yesod master , YesodDispatch master master ) => master -> Maybe (SessionBackend master) -> W.Application toWaiApp' y sb env = case cleanPath y $ W.pathInfo env of Left pieces -> sendRedirect y pieces env Right pieces -> yesodDispatch y y id app404 handler405 method pieces sb env where app404 = yesodRunner notFound y y Nothing id handler405 route = yesodRunner badMethod y y (Just route) id method = decodeUtf8With lenientDecode $ W.requestMethod env 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)) -- | Wrap up a normal WAI application as a Yesod subsite. newtype WaiSubsite = WaiSubsite { runWaiSubsite :: W.Application } instance RenderRoute WaiSubsite where data Route WaiSubsite = WaiSubsiteRoute [Text] [(Text, Text)] deriving (Show, Eq, Read, Ord) renderRoute (WaiSubsiteRoute ps qs) = (ps, qs) instance YesodDispatch WaiSubsite master where yesodDispatch _master (WaiSubsite app) _tomaster _404 _405 _method _pieces _session = app