scottish-0.1.0.0: src/Web/Scottish.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
{- | Scotty apps with configuration and states
-}
module Web.Scottish (
-- * Types
Scottish, Scottish'
, ScottishM, ScottishActionM, ScottishM', ScottishActionM'
-- * App runners/converters
, scottish, scottishApp, scottishOpts
, scottish', scottishApp', scottishOpts', handleRaisedStatus
-- * Configuratio/State accessors
-- ** Shared by 'ScottyM' & 'ScottyActionM'
, getConfig, getGlobalState, getLocalState, (>$<)
-- ** 'ScottyActionM' only
, setLocalState, modifyLocalState
-- ** 'ScottyM' only
, setConfig, modifyConfig
, setGlobalState, modifyGlobalState
-- * Re-exports from Web.Scotty.Trans
, module Trans
) where
import Control.Applicative
import Control.Lens
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Trans.Identity
import Data.Default
import Data.IORef
import qualified Data.Text.Lazy as T
import Network.HTTP.Types.Status
import Network.Wai
import Network.Wai.Handler.Warp (Port)
import Web.Scotty.Trans
import qualified Web.Scotty.Trans as Trans hiding (scottyAppT, scottyOptsT,
scottyT)
data ScottishState c s s' = ScottishState { _config :: c
, _globalState :: s
, _localState :: s'
}
$(makeLenses ''ScottishState)
instance (Default c, Default s, Default s')
=> Default (ScottishState c s s') where
def = ScottishState def def def
-- | 'config' is read-only in 'ActionT', but read-write in 'ScottyT' for
-- initialization.
--
-- 'localState' is reinitialized to 'def' for every execution of each 'ActionT'.
--
-- 'globalState' may be a TVar, or other monadic mutable data types. Normally,
-- 'globalState' should not be necessary for server apps, as there may be
-- multiple instance of the server running, even across machines, and you sure
-- cannot make them share the same state. However, you may be able to do some
-- process-local caching or user interaction (say in games), with 'globalState'.
newtype Scottish config globalState localState a =
Scottish { unScottish :: StateT (ScottishState config globalState
localState) IO a }
deriving (Functor, Monad, Applicative, MonadIO,
MonadState (ScottishState config globalState localState))
-- | 'Scottish' monad without 'globalState'
type Scottish' c s' = Scottish c () s'
type ScottishM e c s s' = ScottyT e (Scottish c s s')
type ScottishActionM e c s s' = ActionT e (Scottish c s s')
instance ScottyError Status where
-- for now, Scotty only `raise`s when input is bad
stringError = either (const badRequest400) id . (toEnum<$>) . readEither
. T.pack
showError = T.pack . show
type ScottishM' c s' = ScottyT Status (Scottish' c s')
type ScottishActionM' c s' = ActionT Status (Scottish' c s')
-- | Run a scottish app with warp.
scottish :: (Default c, Default s, Default s')
=> Port
-> ScottishM e c s s' ()
-> IO ()
scottish p = (mkScottishRunners>>=) . flip (uncurry $ scottyT p)
-- | Turn a scottish app into a WAI one, which can be run with any WAI handler.
scottishApp :: (Default c, Default s, Default s')
=> ScottishM e c s s' ()
-> IO Application
scottishApp = (mkScottishRunners>>=) . flip (uncurry scottyAppT)
-- | Run a scottish app with extra options.
scottishOpts :: (Default c, Default s, Default s')
=> Options -> ScottishM e c s s' () -> IO ()
scottishOpts opts = (mkScottishRunners>>=) . flip (uncurry $ scottyOptsT opts)
-- | Scottish app runner with 'Status' handler installed.
scottish' :: (Default c, Default s, Default s')
=> Port -> ScottishM Status c s s' () -> IO ()
scottish' p = (mkScottishRunners>>=) . flip (uncurry $ scottyT p)
. handleRaisedStatus
-- | Scottish app converter with 'Status' handler installed.
scottishApp' :: (Default c, Default s, Default s')
=> ScottishM Status c s s' () -> IO Application
scottishApp' = (mkScottishRunners>>=) . flip (uncurry scottyAppT)
. handleRaisedStatus
-- | Scottish app runner with 'Status' handler installed.
scottishOpts' :: (Default c, Default s, Default s')
=> Options -> ScottishM Status c s s' () -> IO ()
scottishOpts' opts = (mkScottishRunners>>=) . flip (uncurry $ scottyOptsT opts)
. handleRaisedStatus
mkScottishRunners :: (Default c, Default s, Default s', MonadIO n)
=> IO (forall a. Scottish c s s' a -> n a,
Scottish c s s' Response -> IO Response)
mkScottishRunners = do
shared <- newIORef undefined
let initializer m = liftIO $ do
(r, ss) <- runStateT (unScottish m) def
writeIORef shared ss
return r
actionRunner m = do
ss <- readIORef shared
evalStateT (unScottish m) $ set localState def ss
return (initializer, actionRunner)
getConfig :: (MonadTrans t) => t (Scottish c s s') c
getConfig = lift . use $ config
getGlobalState :: (MonadTrans t) => t (Scottish c s s') s
getGlobalState = lift . use $ globalState
setConfig :: c -> ScottishM e c s s' ()
setConfig = lift . assign config
modifyConfig :: (c -> c) -> ScottishM e c s s' ()
modifyConfig = lift . (config%=)
setGlobalState :: s -> ScottishM e c s s' ()
setGlobalState = lift . assign globalState
modifyGlobalState :: (s -> s) -> ScottishM e c s s' ()
modifyGlobalState = lift . (globalState%=)
getLocalState :: (ScottyError e) => ScottishActionM e c s s' s'
getLocalState = lift . use $ localState
setLocalState :: (ScottyError e) => s' -> ScottishActionM e c s s' ()
setLocalState = lift . assign localState
modifyLocalState:: (ScottyError e) => (s' -> s') -> ScottishActionM e c s s' ()
modifyLocalState = lift . (localState%=)
-- | 'Status' is a good candidate as an 'ScottyError' instance by itself. Call
-- this function to install a default handler to report the 'Status' when one is
-- raised.
--
-- Also, you may want to define instances of 'ScottyError' with tuples/records
-- containing 'Status', to provide more informative error pages.
handleRaisedStatus :: ScottishM Status c s s' () -> ScottishM Status c s s' ()
handleRaisedStatus = ((defaultHandler $ \e -> status e)>>)
-- | Lift a 'Scottish' function to a 'MonadTrans' wrapped 'Scottish' one.
(>$<) :: MonadTrans t
=> (a -> Scottish c s s' b)
-> IdentityT (Scottish c s s') a
-> t (Scottish c s s') b
infixl 4 >$<
f >$< x = lift $ runIdentityT x >>= f