apiary-0.7.0.0: src/Control/Monad/Apiary/Action/Internal.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
module Control.Monad.Apiary.Action.Internal where
import Control.Applicative
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Base
import Control.Monad.Trans.Control
import Network.Wai
import Network.Mime
import Data.Default.Class
import Data.Monoid
import Network.HTTP.Types
import Blaze.ByteString.Builder
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import qualified Data.Text as T
import Data.Conduit
data ApiaryConfig = ApiaryConfig
{ -- | call when no handler matched.
notFound :: Application
-- | used unless call 'status' function.
, defaultStatus :: Status
-- | initial headers.
, defaultHeader :: ResponseHeaders
-- | used by 'Control.Monad.Apiary.Filter.root' filter.
, rootPattern :: [S.ByteString]
, mimeType :: FilePath -> S.ByteString
}
instance Default ApiaryConfig where
def = ApiaryConfig
{ notFound = \_ -> return $ responseLBS status404
[("Content-Type", "text/plain")] "404 Page Notfound.\n"
, defaultStatus = ok200
, defaultHeader = []
, rootPattern = ["", "/", "/index.html", "/index.htm"]
, mimeType = defaultMimeLookup . T.pack
}
data ActionState
= ActionState
{ actionStatus :: Status
, actionHeaders :: ResponseHeaders
, actionBody :: Body
}
data Body
= File FilePath (Maybe FilePart)
| Builder Builder
| LBS L.ByteString
| SRC (Source IO (Flush Builder))
actionStateToResponse :: ActionState -> Response
actionStateToResponse as = case actionBody as of
File f p -> responseFile st hd f p
Builder b -> responseBuilder st hd b
LBS l -> responseLBS st hd l
SRC s -> responseSource st hd s
where
st = actionStatus as
hd = actionHeaders as
data Act a
= Continue a
| Pass
| Stop Response
newtype Action a = Action { unAction :: forall b.
ApiaryConfig
-> Request
-> ActionState
-> (a -> ActionState -> IO (Act b))
-> IO (Act b)
}
instance Functor Action where
fmap f m = Action $ \conf req st cont ->
unAction m conf req st (\a s' -> s' `seq` cont (f a) s')
instance Applicative Action where
pure x = Action $ \_ _ st cont -> cont x st
mf <*> ma = Action $ \conf req st cont ->
unAction mf conf req st $ \f st' ->
unAction ma conf req st' $ \a st'' ->
st' `seq` st'' `seq` cont (f a) st''
instance Monad Action where
return x = Action $ \_ _ st cont -> cont x st
m >>= k = Action $ \conf req st cont ->
unAction m conf req st $ \a st' ->
st' `seq` unAction (k a) conf req st' cont
fail _ = Action $ \_ _ _ _ -> return Pass
instance MonadIO Action where
liftIO m = Action $ \_ _ st cont ->
liftIO m >>= \a -> cont a st
runAction :: Action a
-> ApiaryConfig -> Request -> ActionState
-> IO (Act (a, ActionState))
runAction m conf req st = unAction m conf req st $ \a st' ->
st' `seq` return (Continue (a, st'))
action :: (ApiaryConfig -> Request -> ActionState -> IO (Act (a, ActionState)))
-> Action a
action f = Action $ \conf req st cont -> f conf req st >>= \case
Pass -> return Pass
Stop s -> return $ Stop s
Continue (a,st') -> st' `seq` cont a st'
execAction :: ApiaryConfig -> Action () -> Application
execAction config m request = runAction m config request resp >>= \case
Pass -> notFound config request
Stop s -> return s
Continue (_,r) -> return $ actionStateToResponse r
where
resp = ActionState (defaultStatus config) (defaultHeader config) (LBS "")
instance Alternative Action where
empty = mzero
(<|>) = mplus
instance MonadPlus Action where
mzero = action $ \_ _ _ -> return Pass
mplus m n = action $ \c r s -> runAction m c r s >>= \case
Continue a -> return $ Continue a
Stop stp -> return $ Stop stp
Pass -> runAction n c r s
instance Monoid (Action ()) where
mempty = mzero
mappend = mplus
instance MonadBase IO Action where
liftBase = liftIO
instance MonadBaseControl IO Action where
newtype StM Action a = StMAction { unStMAction :: Act (a, ActionState) }
liftBaseWith f = action $ \c r s ->
liftM (\a -> Continue (a, s)) (f $ \t -> liftM StMAction $ runAction t c r s)
restoreM m = action $ \_ _ _ -> return (unStMAction m)
-- | stop handler and send current state. since 0.3.3.0.
stop :: Action a
stop = Action $ \_ _ s _ -> return $ Stop (actionStateToResponse s)
-- | stop with response. since 0.4.2.0.
stopWith :: Response -> Action a
stopWith a = Action $ \_ _ _ _ -> return $ Stop a
-- | get raw request. since 0.1.0.0.
getRequest :: Action Request
getRequest = Action $ \_ r s c -> c r s
getConfig :: Action ApiaryConfig
getConfig = Action $ \c _ s cont -> cont c s
modifyState :: (ActionState -> ActionState) -> Action ()
modifyState f = Action $ \_ _ s c -> c () (f s)
-- | get all request headers. since 0.6.0.0.
getHeaders :: Action RequestHeaders
getHeaders = requestHeaders `liftM` getRequest
-- | set status code. since 0.1.0.0.
status :: Status -> Action ()
status st = modifyState (\s -> s { actionStatus = st } )
-- | modify response header. since 0.1.0.0.
modifyHeader :: (ResponseHeaders -> ResponseHeaders) -> Action ()
modifyHeader f = modifyState (\s -> s {actionHeaders = f $ actionHeaders s } )
-- | add response header. since 0.1.0.0.
addHeader :: HeaderName -> S.ByteString -> Action ()
addHeader h v = modifyHeader ((h,v):)
-- | set response headers. since 0.1.0.0.
setHeaders :: ResponseHeaders -> Action ()
setHeaders hs = modifyHeader (const hs)
-- | set content-type header.
-- if content-type header already exists, replace it. since 0.1.0.0.
contentType :: S.ByteString -> Action ()
contentType c = modifyHeader
(\h -> ("Content-Type", c) : filter (("Content-Type" /=) . fst) h)
-- | redirect handler
--
-- set status, add location header. since 0.3.3.0.
--
-- rename from redirect in 0.6.2.0.
redirectWith :: Status
-> S.ByteString -- ^ Location redirect to
-> Action ()
redirectWith st url = do
status st
addHeader "location" url
-- HTTP/1.0 HTTP/1.1
-- 300 MultipleChoices
-- 301 MovedPermanently MovedPermanently
-- 302 MovedTemporarily Found
-- 303 SeeOther
-- 304 NotModified NotModified
-- 305 UseProxy
-- 307 TemporaryRedirect
-- | redirect with 301 Moved Permanently. since 0.3.3.0.
redirectPermanently :: S.ByteString -> Action ()
redirectPermanently = redirectWith movedPermanently301
-- | redirect with:
--
-- 303 See Other (HTTP/1.1) or
-- 302 Moved Temporarily (Other)
--
-- since 0.6.2.0.
redirect :: S.ByteString -> Action ()
redirect to = do
v <- httpVersion <$> getRequest
if v == http11
then redirectWith seeOther303 to
else redirectWith status302 to
-- | redirect with:
--
-- 307 Temporary Redirect (HTTP/1.1) or
-- 302 Moved Temporarily (Other)
--
-- since 0.3.3.0.
redirectTemporary :: S.ByteString -> Action ()
redirectTemporary to = do
v <- httpVersion <$> getRequest
if v == http11
then redirectWith temporaryRedirect307 to
else redirectWith status302 to
-- | set response body file content and detect Content-Type by extension. since 0.1.0.0.
file :: FilePath -> Maybe FilePart -> Action ()
file f p = do
mime <- mimeType <$> getConfig
contentType (mime f)
file' f p
-- | set response body file content, without set Content-Type. since 0.1.0.0.
file' :: FilePath -> Maybe FilePart -> Action ()
file' f p = modifyState (\s -> s { actionBody = File f p } )
-- | set response body builder. since 0.1.0.0.
builder :: Builder -> Action ()
builder b = modifyState (\s -> s { actionBody = Builder b } )
-- | set response body lazy bytestring. since 0.1.0.0.
lbs :: L.ByteString -> Action ()
lbs l = modifyState (\s -> s { actionBody = LBS l } )
-- | set response body source. since 0.1.0.0.
source :: Source IO (Flush Builder) -> Action ()
source src = modifyState (\s -> s { actionBody = SRC src } )
{-# DEPRECATED redirectFound, redirectSeeOther "use redirect" #-}
-- | redirect with 302 Found. since 0.3.3.0.
redirectFound :: S.ByteString -> Action ()
redirectFound = redirectWith found302
-- | redirect with 303 See Other. since 0.3.3.0.
redirectSeeOther :: S.ByteString -> Action ()
redirectSeeOther = redirectWith seeOther303