ytl-0.1.0.0: src/Yesod/Trans/TH.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Yesod.Trans.TH
( defaultYesodInstanceExcept
) where
import Yesod.Site.Util
import Yesod.Trans.Class
import Data.Coerce
import Yesod.Core
( Approot (..)
, guessApproot
, HtmlUrl
, RenderRoute (..)
, ScriptLoadPosition (..)
, Yesod (..)
)
import Language.Haskell.TH
coerceHtmlUrl
:: SiteCompatible site site'
=> HtmlUrl (Route site) -> HtmlUrl (Route site')
coerceHtmlUrl url = url . (. coerce)
-- | Fills in an instance for the 'Yesod' class for a 'SiteTrans' wrapper with
-- implementations which just invoke the base class, *except* for those implementations
-- which are defined on the instance itself.
--
-- This is useful for 'SiteTrans' implementations that want to modify some of
-- the 'Yesod' behaviour of the site, but mostly want to delegate behaviour to
-- the base site. Instead of writing out a whole 'Yesod' instance, you can just
-- override the class methods that you need. The rest will be filled in with
-- working implementations that do what you expect.
defaultYesodInstanceExcept
:: Q Exp -- ^ How to go from the wrapped site to the base site.
-- If the instance is for a type of the form 't site', then this
-- should be an expression of type 't site -> site'. This operation
-- is necessary to use some of the default implementations.
-> Q [Dec] -- ^ The partial 'Yesod' instance. Should just be a since @instance@
-- declaration, except that its body can be as empty as you like. For
-- example:
--
-- @
-- defaultYesodInstanceExcept [| myLowerer |] [d|
-- instance (Yesod site) => Yesod (MyWrapper site) where
-- yesodMiddleware = ... -- insert some middleware
--
-- -- But everything else should be defined in the default way
-- |]
-- @
--
-- This declaration will include the custom definition for
-- 'yesodMiddleware', as well as implementations for the other
-- class methods that just delegate to the base class.
-> Q [Dec]
defaultYesodInstanceExcept baseSite partialInstanceQ = do
[InstanceD overlap ctxt instanceHead exceptions] <- partialInstanceQ
defaultImplementations <- defaultImplementationsQ
let fullBody = exceptions <> (filter (`undeclaredIn` exceptions) defaultImplementations)
pure [InstanceD overlap ctxt instanceHead fullBody]
where
decName :: Dec -> Maybe Name
decName (FunD name _) = Just name
decName (ValD (VarP name) _ _) = Just name
decName _ = Nothing
undeclaredIn :: Dec -> [Dec] -> Bool
undeclaredIn dec
| Just name <- decName dec
= not . any (maybe False (== name) . decName)
undeclaredIn _ = const True
defaultImplementationsQ = [d|
$(pure $ VarP 'approot) = case $([|approot|]) of
ApprootRelative -> ApprootRelative
ApprootStatic t -> ApprootStatic t
ApprootMaster f -> ApprootMaster (f . $(baseSite))
ApprootRequest f -> ApprootRequest (f. $(baseSite))
-- Approot is non-exhaustive, so for API compatibility, we need a
-- (apparently redundant) fallthrough case
_ -> guessApproot
$(pure $ VarP 'errorHandler) = lift . $([|errorHandler|])
$(pure $ VarP 'defaultLayout) = mapSiteT $([|defaultLayout|])
$(pure $ VarP 'urlParamRenderOverride) = \site route ->
$([|urlParamRenderOverride|]) ($(baseSite) site) (coerce route)
$(pure $ VarP 'isAuthorized) = \route isWrite ->
lift ($([|isAuthorized|]) (coerce route) isWrite)
$(pure $ VarP 'isWriteRequest)
= lift . $([|isWriteRequest|]) . coerce
$(pure $ VarP 'authRoute)
= fmap coerce . $([|authRoute|]) . $(baseSite)
$(pure $ VarP 'cleanPath) = $([|cleanPath|]) . $(baseSite)
$(pure $ VarP 'joinPath) = $([|joinPath|]) . $(baseSite)
$(pure $ VarP 'addStaticContent) = \fn mime content -> do
ret <- lift $ $([|addStaticContent|]) fn mime content
pure $ case ret of
Nothing -> Nothing
Just (Left t) -> Just (Left t)
Just (Right (route, params))
-> Just (Right (coerce route, params))
$(pure $ VarP 'maximumContentLength) = \site mRoute ->
$([|maximumContentLength|]) ($(baseSite) site) (coerce <$> mRoute)
$(pure $ VarP 'maximumContentLengthIO) = \site mRoute ->
$([|maximumContentLengthIO|]) ($(baseSite) site) (coerce <$> mRoute)
$(pure $ VarP 'makeLogger)
= $([|makeLogger|]) . $(baseSite)
$(pure $ VarP 'messageLoggerSource)
= $([|messageLoggerSource|]) . $(baseSite)
$(pure $ VarP 'jsLoader) = \site ->
case $([|jsLoader|]) ($(baseSite) site) of
BottomOfBody -> BottomOfBody
BottomOfHeadBlocking -> BottomOfHeadBlocking
BottomOfHeadAsync async
-> BottomOfHeadAsync
(\urls mHtml ->
coerceHtmlUrl $ async urls (coerceHtmlUrl <$> mHtml))
$(pure $ VarP 'jsAttributes)
= $([|jsAttributes|]) . $(baseSite)
$(pure $ VarP 'jsAttributesHandler)
= lift $([|jsAttributesHandler|])
$(pure $ VarP 'makeSessionBackend)
= $([|makeSessionBackend|]) . $(baseSite)
$(pure $ VarP 'fileUpload)
= $([|fileUpload|]) . $(baseSite)
$(pure $ VarP 'shouldLogIO)
= $([|shouldLogIO|]) . $(baseSite)
$(pure $ VarP 'yesodMiddleware)
= mapSiteT $([|yesodMiddleware|])
$(pure $ VarP 'yesodWithInternalState) = \site mRoute ->
$([|yesodWithInternalState|]) ($(baseSite) site) (coerce <$> mRoute)
$(pure $ VarP 'defaultMessageWidget) = \html url ->
lift $ $([|defaultMessageWidget|]) html (url . coerce)
|]