snap-0.11.0: src/Snap/Snaplet/HeistNoClass.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-|
This module implements the Heist snaplet without using type classes. It is
provided mainly as an example of how snaplets can be written with and without
a type class for convenience.
-}
module Snap.Snaplet.HeistNoClass
( Heist
, heistInit
, heistInit'
, clearHeistCache
, addTemplates
, addTemplatesAt
, modifyHeistState
, modifyHeistState'
, withHeistState
, withHeistState'
, addSplices
, addSplices'
, addConfig
, cRender
, cRenderAs
, cHeistServe
, cHeistServeSingle
, render
, renderAs
, heistServe
, heistServeSingle
, heistLocal
, withSplices
, renderWithSplices
, heistLocal'
, withSplices'
, renderWithSplices'
, SnapletHeist
, SnapletISplice
, SnapletCSplice
) where
import Prelude hiding ((.), id)
import Control.Applicative
import Control.Category
import Control.Comonad
import Control.Error
import Control.Monad.Reader
import Control.Monad.State
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.DList (DList)
import qualified Data.HashMap.Strict as Map
import Data.IORef
import Data.List
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding
import System.FilePath.Posix
import Heist
import qualified Heist.Compiled as C
import qualified Heist.Interpreted as I
import Heist.Splices.Cache
import Snap.Snaplet
import Snap.Core
import Snap.Util.FileServe
------------------------------------------------------------------------------
-- | The state for the Heist snaplet. To use the Heist snaplet in your app
-- include this in your application state and use 'heistInit' to initialize
-- it. The type parameter b will typically be the base state type for your
-- application.
data Heist b = Configuring
{ _heistConfig :: IORef (HeistConfig (Handler b b)) }
| Running
{ _heistState :: HeistState (Handler b b)
, _heistCTS :: CacheTagState
}
------------------------------------------------------------------------------
changeState :: (HeistState (Handler a a) -> HeistState (Handler a a))
-> Heist a
-> Heist a
changeState _ (Configuring _) =
error "changeState: HeistState has not been initialized"
changeState f (Running hs cts) = Running (f hs) cts
------------------------------------------------------------------------------
-- | Clears data stored by the cache tag. The cache tag automatically reloads
-- its data when the specified TTL expires, but sometimes you may want to
-- trigger a manual reload. This function lets you do that.
clearHeistCache :: Heist b -> IO ()
clearHeistCache = clearCacheTagState . _heistCTS
-----------------------------
-- SnapletSplice functions --
-----------------------------
------------------------------------------------------------------------------
-- | This instance is here because we don't want the heist package to depend
-- on anything from snap packages.
instance MonadSnap m => MonadSnap (HeistT n m) where
liftSnap = lift . liftSnap
type SnapletHeist b m a = HeistT (Handler b b) m a
type SnapletCSplice b = SnapletHeist b IO (DList (Chunk (Handler b b)))
type SnapletISplice b = SnapletHeist b (Handler b b) Template
---------------------------
-- Initializer functions --
---------------------------
------------------------------------------------------------------------------
-- | The 'Initializer' for 'Heist'. This function is a convenience wrapper
-- around `heistInit'` that uses defaultHeistState and sets up routes for all
-- the templates.
heistInit :: FilePath
-- ^ Path to templates
-> SnapletInit b (Heist b)
heistInit templateDir = do
makeSnaplet "heist" "" Nothing $ do
hs <- heistInitWorker templateDir defaultConfig
addRoutes [ ("", heistServe) ]
return hs
where
defaultConfig = mempty { hcLoadTimeSplices = defaultLoadTimeSplices }
------------------------------------------------------------------------------
-- | A lower level 'Initializer' for 'Heist'. This initializer requires you
-- to specify the initial HeistConfig. It also does not add any routes for
-- templates, allowing you complete control over which templates get routed.
heistInit' :: FilePath
-- ^ Path to templates
-> HeistConfig (Handler b b)
-- ^ Initial HeistConfig
-> SnapletInit b (Heist b)
heistInit' templateDir initialConfig =
makeSnaplet "heist" "" Nothing $ heistInitWorker templateDir initialConfig
------------------------------------------------------------------------------
-- | Internal worker function used by variantsof heistInit. This is necessary
-- because of the divide between SnapletInit and Initializer.
heistInitWorker :: FilePath
-> HeistConfig (Handler b b)
-> Initializer b (Heist b) (Heist b)
heistInitWorker templateDir initialConfig = do
snapletPath <- getSnapletFilePath
let tDir = snapletPath </> templateDir
templates <- liftIO $ runEitherT (loadTemplates tDir) >>=
either (error . concat) return
let config = initialConfig `mappend` mempty { hcTemplates = templates }
printInfo $ T.pack $ unwords
[ "...loaded"
, (show $ Map.size templates)
, "templates from"
, tDir
]
ref <- liftIO $ newIORef config
addPostInitHook finalLoadHook
return $ Configuring ref
------------------------------------------------------------------------------
-- | Hook that converts the Heist type from Configuring to Running at the end
-- of initialization.
finalLoadHook :: Heist b -> EitherT Text IO (Heist b)
finalLoadHook (Configuring ref) = do
hc <- lift $ readIORef ref
(hs,cts) <- toTextErrors $ initHeistWithCacheTag hc
return $ Running hs cts
where
toTextErrors = bimapEitherT (T.pack . intercalate "\n") id
finalLoadHook (Running _ _) = left "finalLoadHook called while running"
------------------------------------------------------------------------------
-- | Adds templates to the Heist HeistConfig. Other snaplets should use
-- this function to add their own templates. The templates are automatically
-- read from the templates directory in the current snaplet's filesystem root.
addTemplates :: Snaplet (Heist b)
-> ByteString
-- ^ The url prefix for the template routes
-> Initializer b (Heist b) ()
addTemplates h urlPrefix = do
snapletPath <- getSnapletFilePath
addTemplatesAt h urlPrefix (snapletPath </> "templates")
------------------------------------------------------------------------------
-- | Adds templates to the Heist HeistConfig, and lets you specify where
-- they are found in the filesystem. Note that the path to the template
-- directory is an absolute path. This allows you more flexibility in where
-- your templates are located, but means that you have to explicitly call
-- getSnapletFilePath if you want your snaplet to use templates within its
-- normal directory structure.
addTemplatesAt :: Snaplet (Heist b)
-> ByteString
-- ^ URL prefix for template routes
-> FilePath
-- ^ Path to templates
-> Initializer b (Heist b) ()
addTemplatesAt h urlPrefix templateDir = do
rootUrl <- getSnapletRootURL
let fullPrefix = (T.unpack $ decodeUtf8 rootUrl) </>
(T.unpack $ decodeUtf8 urlPrefix)
addPrefix = return . addTemplatePathPrefix
(encodeUtf8 $ T.pack fullPrefix)
ts <- liftIO $ runEitherT (loadTemplates templateDir) >>=
either (error . concat) addPrefix
printInfo $ T.pack $ unwords
[ "...adding"
, (show $ Map.size ts)
, "templates from"
, templateDir
, "with route prefix"
, fullPrefix ++ "/"
]
liftIO $ atomicModifyIORef (_heistConfig $ extract h)
(\hc -> (hc `mappend` mempty { hcTemplates = ts }, ()))
------------------------------------------------------------------------------
modifyHeistState' :: SnapletLens (Snaplet b) (Heist b)
-> (HeistState (Handler b b) -> HeistState (Handler b b))
-> Initializer b v ()
modifyHeistState' heist f = do
withTop' heist $ addPostInitHook $ return . changeState f
------------------------------------------------------------------------------
modifyHeistState :: SnapletLens b (Heist b)
-> (HeistState (Handler b b) -> HeistState (Handler b b))
-> Initializer b v ()
modifyHeistState heist f = modifyHeistState' (subSnaplet heist) f
------------------------------------------------------------------------------
withHeistState' :: SnapletLens (Snaplet b) (Heist b)
-> (HeistState (Handler b b) -> a)
-> Handler b v a
withHeistState' heist f = do
hs <- withTop' heist $ gets _heistState
return $ f hs
------------------------------------------------------------------------------
withHeistState :: SnapletLens b (Heist b)
-> (HeistState (Handler b b) -> a)
-> Handler b v a
withHeistState heist f = withHeistState' (subSnaplet heist) f
------------------------------------------------------------------------------
-- | Adds more HeistConfig data using mappend with whatever is currently
-- there. This is the preferred method for adding all four kinds of splices
-- as well as new templates.
addConfig :: Snaplet (Heist b)
-> HeistConfig (Handler b b)
-> Initializer b v ()
addConfig h hc = case extract h of
Configuring ref ->
liftIO $ atomicModifyIORef ref (\hc1 -> (hc1 `mappend` hc, ()))
Running _ _ -> do
printInfo "finalLoadHook called while running"
error "this shouldn't happen"
------------------------------------------------------------------------------
addSplices' :: SnapletLens (Snaplet b) (Heist b)
-> [(Text, SnapletISplice b)]
-> Initializer b v ()
addSplices' heist splices = do
withTop' heist $ addPostInitHook $
return . changeState (I.bindSplices splices)
------------------------------------------------------------------------------
addSplices :: SnapletLens b (Heist b)
-> [(Text, SnapletISplice b)]
-> Initializer b v ()
addSplices heist splices = addSplices' (subSnaplet heist) splices
-----------------------
-- Handler functions --
-----------------------
------------------------------------------------------------------------------
-- | Internal helper function for rendering.
iRenderHelper :: Maybe MIMEType
-> ByteString
-> Handler b (Heist b) ()
iRenderHelper c t = do
(Running hs _) <- get
withTop' id $ I.renderTemplate hs t >>= maybe pass serve
where
serve (b, mime) = do
modifyResponse $ setContentType $ fromMaybe mime c
writeBuilder b
------------------------------------------------------------------------------
-- | Internal helper function for rendering.
cRenderHelper :: Maybe MIMEType
-> ByteString
-> Handler b (Heist b) ()
cRenderHelper c t = do
(Running hs _) <- get
withTop' id $ maybe pass serve $ C.renderTemplate hs t
where
serve (b, mime) = do
modifyResponse $ setContentType $ fromMaybe mime c
writeBuilder =<< b
------------------------------------------------------------------------------
render :: ByteString
-- ^ Name of the template
-> Handler b (Heist b) ()
render t = iRenderHelper Nothing t
------------------------------------------------------------------------------
renderAs :: ByteString
-- ^ Content type
-> ByteString
-- ^ Name of the template
-> Handler b (Heist b) ()
renderAs ct t = iRenderHelper (Just ct) t
------------------------------------------------------------------------------
cRender :: ByteString
-- ^ Name of the template
-> Handler b (Heist b) ()
cRender t = cRenderHelper Nothing t
------------------------------------------------------------------------------
cRenderAs :: ByteString
-- ^ Content type
-> ByteString
-- ^ Name of the template
-> Handler b (Heist b) ()
cRenderAs ct t = cRenderHelper (Just ct) t
------------------------------------------------------------------------------
serveURI :: Handler b (Heist b) ByteString
serveURI = do
p <- getSafePath
-- Allows users to prefix template filenames with an underscore to prevent
-- the template from being served.
if take 1 p == "_" then pass else return $ B.pack p
------------------------------------------------------------------------------
heistServe :: Handler b (Heist b) ()
heistServe =
ifTop (render "index") <|> (render =<< serveURI)
------------------------------------------------------------------------------
heistServeSingle :: ByteString -> Handler b (Heist b) ()
heistServeSingle t =
render t <|> error ("Template " ++ show t ++ " not found.")
------------------------------------------------------------------------------
cHeistServe :: Handler b (Heist b) ()
cHeistServe =
ifTop (cRender "index") <|> (cRender =<< serveURI)
------------------------------------------------------------------------------
cHeistServeSingle :: ByteString -> Handler b (Heist b) ()
cHeistServeSingle t =
cRender t <|> error ("Template " ++ show t ++ " not found.")
------------------------------------------------------------------------------
heistLocal' :: SnapletLens (Snaplet b) (Heist b)
-> (HeistState (Handler b b) -> HeistState (Handler b b))
-> Handler b v a
-> Handler b v a
heistLocal' heist f m = do
hs <- withTop' heist get
withTop' heist $ modify $ changeState f
res <- m
withTop' heist $ put hs
return res
------------------------------------------------------------------------------
heistLocal :: SnapletLens b (Heist b)
-> (HeistState (Handler b b) -> HeistState (Handler b b))
-> Handler b v a
-> Handler b v a
heistLocal heist f m = heistLocal' (subSnaplet heist) f m
------------------------------------------------------------------------------
withSplices' :: SnapletLens (Snaplet b) (Heist b)
-> [(Text, SnapletISplice b)]
-> Handler b v a
-> Handler b v a
withSplices' heist splices m = do
heistLocal' heist (I.bindSplices splices) m
------------------------------------------------------------------------------
withSplices :: SnapletLens b (Heist b)
-> [(Text, SnapletISplice b)]
-> Handler b v a
-> Handler b v a
withSplices heist splices m = withSplices' (subSnaplet heist) splices m
------------------------------------------------------------------------------
renderWithSplices' :: SnapletLens (Snaplet b) (Heist b)
-> ByteString
-> [(Text, SnapletISplice b)]
-> Handler b v ()
renderWithSplices' heist t splices =
withSplices' heist splices $ withTop' heist $ render t
------------------------------------------------------------------------------
renderWithSplices :: SnapletLens b (Heist b)
-> ByteString
-> [(Text, SnapletISplice b)]
-> Handler b v ()
renderWithSplices heist t splices =
renderWithSplices' (subSnaplet heist) t splices