snap-0.12.0: src/Snap/Snaplet/Internal/Types.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleInstances #-}
#ifndef MIN_VERSION_comonad
#define MIN_VERSION_comonad(x,y,z) 1
#endif
module Snap.Snaplet.Internal.Types where
import Control.Applicative
import Control.Error
import Control.Lens
import Control.Monad.CatchIO hiding (Handler)
import Control.Monad.Reader
import Control.Monad.State.Class
import Control.Monad.Trans.Writer hiding (pass)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Configurator.Types
import Data.IORef
import Data.Monoid
import Data.Text (Text)
import Snap.Core
import qualified Snap.Snaplet.Internal.LensT as LT
import qualified Snap.Snaplet.Internal.Lensed as L
------------------------------------------------------------------------------
-- | An opaque data type holding internal snaplet configuration data. It is
-- exported publicly because the getOpaqueConfig function in MonadSnaplet
-- makes implementing new instances of MonadSnaplet more convenient.
data SnapletConfig = SnapletConfig
{ _scAncestry :: [Text]
, _scFilePath :: FilePath
, _scId :: Maybe Text
, _scDescription :: Text
, _scUserConfig :: Config
, _scRouteContext :: [ByteString]
, _scRoutePattern :: Maybe ByteString
-- ^ Holds the actual route pattern passed to addRoutes for the
-- current handler. Nothing during initialization and before route
-- dispatech.
, _reloader :: IO (Either Text Text) -- might change
-- ^ This is the universal reload action for the top-level site. We
-- can't update this in place to be a reloader for each individual
-- snaplet because individual snaplets can't be reloaded in isolation
-- without losing effects that subsequent hooks may have had.
}
makeLenses ''SnapletConfig
------------------------------------------------------------------------------
-- | Joins a reversed list of directories into a path.
buildPath :: [ByteString] -> ByteString
buildPath ps = B.intercalate "/" $ filter (not . B.null) $ reverse ps
------------------------------------------------------------------------------
-- | Joins a reversed list of directories into a path.
getRootURL :: SnapletConfig -> ByteString
getRootURL sc = buildPath $ _scRouteContext sc
------------------------------------------------------------------------------
-- | Snaplet's type parameter 's' here is user-defined and can be any Haskell
-- type. A value of type @Snaplet s@ countains a couple of things:
--
-- * a value of type @s@, called the \"user state\".
--
-- * some bookkeeping data the framework uses to plug things together, like
-- the snaplet's configuration, the snaplet's root directory on the
-- filesystem, the snaplet's root URL, and so on.
data Snaplet s = Snaplet
{ _snapletConfig :: SnapletConfig
, _snapletModifier :: s -> IO ()
-- ^ See the _reloader comment for why we have to use this to reload
-- single snaplets in isolation. This action won't actually run the
-- initializer at all. It will only modify the existing state. It is
-- the responsibility of the snaplet author to avoid using this in
-- situations where it will destroy data in its state that was created
-- by subsequent hook actions.
, _snapletValue :: s
}
makeLenses ''Snaplet
--instance Functor Snaplet where
-- fmap f (Snaplet c r a) = Snaplet c r (f a)
--
--instance Foldable Snaplet where
-- foldMap f (Snaplet _ _ a) = f a
--
--instance Traversable Snaplet where
-- traverse f (Snaplet c r a) = Snaplet c r <$> f a
--
--instance Comonad Snaplet where
-- extract (Snaplet _ _ a) = a
--
-- #if !(MIN_VERSION_comonad(3,0,0))
-- instance Extend Snaplet where
-- #endif
-- extend f w@(Snaplet c r _) = Snaplet c r (f w)
{-
------------------------------------------------------------------------------
-- | A lens referencing the opaque SnapletConfig data type held inside
-- Snaplet.
snapletConfig :: SimpleLens (Snaplet a) SnapletConfig
------------------------------------------------------------------------------
-- | A lens referencing the user-defined state type wrapped by a Snaplet.
snapletValue :: SimpleLens (Snaplet a) a
-}
type SnapletLens s a = ALens' s (Snaplet a)
------------------------------------------------------------------------------
-- | Transforms a lens of the type you get from makeLenses to an similar lens
-- that is more suitable for internal use.
subSnaplet :: SnapletLens a b
-> SnapletLens (Snaplet a) b
subSnaplet l = snapletValue . l
------------------------------------------------------------------------------
-- | The m type parameter used in the MonadSnaplet type signatures will
-- usually be either Initializer or Handler, but other monads may sometimes be
-- useful.
--
-- Minimal complete definition:
--
-- * 'withTop'', 'with'', 'getLens', and 'getOpaqueConfig'.
--
class MonadSnaplet m where
-- | Runs a child snaplet action in the current snaplet's context. If you
-- think about snaplet lenses using a filesystem path metaphor, the lens
-- supplied to this snaplet must be a relative path. In other words, the
-- lens's base state must be the same as the current snaplet.
with :: SnapletLens v v'
-- ^ A relative lens identifying a snaplet
-> m b v' a
-- ^ Action from the lense's snaplet
-> m b v a
with l = with' (subSnaplet l)
-- | Like 'with' but doesn't impose the requirement that the action
-- being run be a descendant of the current snaplet. Using our filesystem
-- metaphor again, the lens for this function must be an absolute
-- path--it's base must be the same as the current base.
withTop :: SnapletLens b v'
-- ^ An \"absolute\" lens identifying a snaplet
-> m b v' a
-- ^ Action from the lense's snaplet
-> m b v a
withTop l = withTop' (subSnaplet l)
-- | A variant of 'with' accepting a lens from snaplet to snaplet. Unlike
-- the lens used in the above 'with' function, this lens formulation has
-- an identity, which makes it useful in certain circumstances. The
-- lenses generated by 'makeLenses' will not work with this function,
-- however the lens returned by 'getLens' will.
--
-- @with = with' . subSnaplet@
with' :: SnapletLens (Snaplet v) v'
-> m b v' a -> m b v a
-- Not providing a definition for this function in terms of withTop'
-- allows us to avoid extra Monad type class constraints, making the type
-- signature easier to read.
-- with' l m = flip withTop m . (l .) =<< getLens
-- | The absolute version of 'with''
withTop' :: SnapletLens (Snaplet b) v'
-> m b v' a -> m b v a
-- | Gets the lens for the current snaplet.
getLens :: m b v (SnapletLens (Snaplet b) v)
-- | Gets the current snaplet's opaque config data type. You'll only use
-- this function when writing MonadSnaplet instances.
getOpaqueConfig :: m b v SnapletConfig
-- NOTE: We can't just use a MonadState (Snaplet v) instance for this
-- because Initializer has SnapletConfig, but doesn't have a full Snaplet.
------------------------------------------------------------------------------
-- | Gets a list of the names of snaplets that are direct ancestors of the
-- current snaplet.
getSnapletAncestry :: (Monad (m b v), MonadSnaplet m) => m b v [Text]
getSnapletAncestry = return . _scAncestry =<< getOpaqueConfig
------------------------------------------------------------------------------
-- | Gets the snaplet's path on the filesystem.
getSnapletFilePath :: (Monad (m b v), MonadSnaplet m) => m b v FilePath
getSnapletFilePath = return . _scFilePath =<< getOpaqueConfig
------------------------------------------------------------------------------
-- | Gets the current snaple's name.
getSnapletName :: (Monad (m b v), MonadSnaplet m) => m b v (Maybe Text)
getSnapletName = return . _scId =<< getOpaqueConfig
------------------------------------------------------------------------------
-- | Gets a human readable description of the snaplet.
getSnapletDescription :: (Monad (m b v), MonadSnaplet m) => m b v Text
getSnapletDescription = return . _scDescription =<< getOpaqueConfig
------------------------------------------------------------------------------
-- | Gets the config data structure for the current snaplet.
getSnapletUserConfig :: (Monad (m b v), MonadSnaplet m) => m b v Config
getSnapletUserConfig = return . _scUserConfig =<< getOpaqueConfig
------------------------------------------------------------------------------
-- | Gets the base URL for the current snaplet. Directories get added to
-- the current snaplet path by calls to 'nestSnaplet'.
getSnapletRootURL :: (Monad (m b v), MonadSnaplet m) => m b v ByteString
getSnapletRootURL = liftM getRootURL getOpaqueConfig
------------------------------------------------------------------------------
-- | Constructs a url relative to the current snaplet.
snapletURL :: (Monad (m b v), MonadSnaplet m)
=> ByteString -> m b v ByteString
snapletURL suffix = do
cfg <- getOpaqueConfig
return $ buildPath (cleanSuffix : _scRouteContext cfg)
where
dropSlash = B.dropWhile (=='/')
cleanSuffix = B.reverse $ dropSlash $ B.reverse $ dropSlash suffix
------------------------------------------------------------------------------
-- | Snaplet infrastructure is available during runtime request processing
-- through the Handler monad. There aren't very many standalone functions to
-- read about here, but this is deceptive. The key is in the type class
-- instances. Handler is an instance of 'MonadSnap', which means it is the
-- monad you will use to write all your application routes. It also has a
-- 'MonadSnaplet' instance, which gives you all the functionality described
-- above.
newtype Handler b v a =
Handler (L.Lensed (Snaplet b) (Snaplet v) Snap a)
deriving ( Monad
, Functor
, Applicative
, MonadIO
, MonadPlus
, MonadCatchIO
, Alternative
, MonadSnap)
------------------------------------------------------------------------------
-- | Gets the @Snaplet v@ from the current snaplet's state.
getSnapletState :: Handler b v (Snaplet v)
getSnapletState = Handler get
------------------------------------------------------------------------------
-- | Puts a new @Snaplet v@ in the current snaplet's state.
putSnapletState :: Snaplet v -> Handler b v ()
putSnapletState = Handler . put
------------------------------------------------------------------------------
-- | Modifies the @Snaplet v@ in the current snaplet's state.
modifySnapletState :: (Snaplet v -> Snaplet v) -> Handler b v ()
modifySnapletState f = do
s <- getSnapletState
putSnapletState (f s)
------------------------------------------------------------------------------
-- | Gets the @Snaplet v@ from the current snaplet's state and applies a
-- function to it.
getsSnapletState :: (Snaplet v -> b) -> Handler b1 v b
getsSnapletState f = do
s <- getSnapletState
return (f s)
------------------------------------------------------------------------------
-- | The MonadState instance gives you access to the current snaplet's state.
instance MonadState v (Handler b v) where
get = getsSnapletState _snapletValue
put v = modifySnapletState (set snapletValue v)
------------------------------------------------------------------------------
-- | The MonadState instance gives you access to the current snaplet's state.
instance MonadReader v (Handler b v) where
ask = getsSnapletState _snapletValue
local f m = do
cur <- ask
put (f cur)
res <- m
put cur
return res
instance MonadSnaplet Handler where
getLens = Handler ask
with' !l (Handler !m) = Handler $ L.with l m
withTop' !l (Handler m) = Handler $ L.withTop l m
getOpaqueConfig = Handler $ gets _snapletConfig
------------------------------------------------------------------------------
-- | Like 'runBase', but it doesn't require an MVar to be executed.
runPureBase :: Handler b b a -> Snaplet b -> Snap a
runPureBase (Handler m) b = do
(!a, _) <- L.runLensed m id b
return $! a
------------------------------------------------------------------------------
-- | Gets the route pattern that matched for the handler. This lets you find
-- out exactly which of the strings you used in addRoutes matched.
getRoutePattern :: Handler b v (Maybe ByteString)
getRoutePattern =
withTop' id $ liftM _scRoutePattern getOpaqueConfig
------------------------------------------------------------------------------
-- | Sets the route pattern that matched for the handler. Use this when to
-- override the default pattern which is the key to the alist passed to
-- addRoutes.
setRoutePattern :: ByteString -> Handler b v ()
setRoutePattern p = withTop' id $
modifySnapletState (set (snapletConfig . scRoutePattern) (Just p))
------------------------------------------------------------------------------
-- | Pass if the request is not coming from localhost.
failIfNotLocal :: MonadSnap m => m b -> m b
failIfNotLocal m = do
-- FIXME: this moves to auth once control-panel is done
rip <- liftM rqRemoteAddr getRequest
if not $ elem rip [ "127.0.0.1"
, "localhost"
, "::1" ]
then pass
else m
------------------------------------------------------------------------------
-- | Handler that reloads the site.
reloadSite :: Handler b v ()
reloadSite = failIfNotLocal $ do
cfg <- getOpaqueConfig
!res <- liftIO $ _reloader cfg
either bad good res
where
bad msg = do
writeText $ "Error reloading site!\n\n"
writeText msg
good msg = do
writeText msg
writeText $ "Site successfully reloaded.\n"
------------------------------------------------------------------------------
-- | This function brackets a Handler action in resource acquisition and
-- release. Like 'bracketSnap', this is provided because MonadCatchIO's
-- 'bracket' function doesn't work properly in the case of a short-circuit
-- return from the action being bracketed.
--
-- In order to prevent confusion regarding the effects of the
-- aquisition and release actions on the Handler state, this function
-- doesn't accept Handler actions for the acquire or release actions.
--
-- This function will run the release action in all cases where the
-- acquire action succeeded. This includes the following behaviors
-- from the bracketed Snap action.
--
-- 1. Normal completion
--
-- 2. Short-circuit completion, either from calling 'fail' or 'finishWith'
--
-- 3. An exception being thrown.
bracketHandler :: IO a -> (a -> IO x) -> (a -> Handler b v c) -> Handler b v c
bracketHandler begin end f = Handler . L.Lensed $ \l v b -> do
bracketSnap begin end $ \a -> case f a of Handler m -> L.unlensed m l v b
------------------------------------------------------------------------------
-- | Information about a partially constructed initializer. Used to
-- automatically aggregate handlers and cleanup actions.
data InitializerState b = InitializerState
{ _isTopLevel :: Bool
, _cleanup :: IORef (IO ())
, _handlers :: [(ByteString, Handler b b ())]
-- ^ Handler routes built up and passed to route.
, _hFilter :: Handler b b () -> Handler b b ()
-- ^ Generic filtering of handlers
, _curConfig :: SnapletConfig
-- ^ This snaplet config is the incrementally built config for
-- whatever snaplet is currently being constructed.
, _initMessages :: IORef Text
, _environment :: String
, masterReloader :: (Snaplet b -> Snaplet b) -> IO ()
-- ^ We can't just hae a simple MVar here because MVars can't be
-- chrooted.
}
------------------------------------------------------------------------------
-- | Wrapper around IO actions that modify state elements created during
-- initialization.
newtype Hook a = Hook (Snaplet a -> EitherT Text IO (Snaplet a))
instance Monoid (Hook a) where
mempty = Hook return
(Hook a) `mappend` (Hook b) = Hook (a >=> b)
------------------------------------------------------------------------------
-- | Monad used for initializing snaplets.
newtype Initializer b v a =
Initializer (LT.LensT (Snaplet b)
(Snaplet v)
(InitializerState b)
(WriterT (Hook b) IO)
a)
deriving (Applicative, Functor, Monad, MonadIO)
makeLenses ''InitializerState
instance MonadSnaplet Initializer where
getLens = Initializer ask
with' !l (Initializer !m) = Initializer $ LT.with l m
withTop' !l (Initializer m) = Initializer $ LT.withTop l m
getOpaqueConfig = Initializer $ liftM _curConfig LT.getBase
------------------------------------------------------------------------------
-- | Opaque newtype which gives us compile-time guarantees that the user is
-- using makeSnaplet and either nestSnaplet or embedSnaplet correctly.
newtype SnapletInit b v = SnapletInit (Initializer b v (Snaplet v))