module Web.Scotty.Action
( request, body, param, jsonData
, status, header, redirect
, text, html, file, json, source
, raise, rescue, next
, ActionM, Parsable(..), readEither, Param, runAction
) where
import Blaze.ByteString.Builder (Builder, fromLazyByteString)
import Control.Applicative
import Control.Monad.Error
import Control.Monad.Reader
import qualified Control.Monad.State as MS
import qualified Data.Aeson as A
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.CaseInsensitive as CI
import Data.Conduit (Flush, ResourceT, Source)
import Data.Default (Default, def)
import Data.Monoid (mconcat)
import qualified Data.Text.Lazy as T
import Data.Text.Lazy.Encoding (encodeUtf8)
import Network.HTTP.Types
import Network.Wai
import Web.Scotty.Types
import Web.Scotty.Util
runAction :: ActionEnv -> ActionM () -> IO (Maybe Response)
runAction env action = do
(e,r) <- flip MS.runStateT def
$ flip runReaderT env
$ runErrorT
$ runAM
$ action `catchError` defaultHandler
return $ either (const Nothing) (const $ Just r) e
defaultHandler :: ActionError -> ActionM ()
defaultHandler (Redirect url) = do
status status302
header "Location" url
defaultHandler (ActionError msg) = do
status status500
html $ mconcat ["<h1>500 Internal Server Error</h1>", msg]
defaultHandler Next = next
raise :: T.Text -> ActionM a
raise = throwError . ActionError
next :: ActionM a
next = throwError Next
rescue :: ActionM a -> (T.Text -> ActionM a) -> ActionM a
rescue action handler = catchError action $ \e -> case e of
ActionError msg -> handler msg
other -> throwError other
redirect :: T.Text -> ActionM a
redirect = throwError . Redirect
request :: ActionM Request
request = getReq <$> ask
body :: ActionM BL.ByteString
body = getBody <$> ask
jsonData :: (A.FromJSON a) => ActionM a
jsonData = do
b <- body
maybe (raise "jsonData: no parse") return $ A.decode b
param :: (Parsable a) => T.Text -> ActionM a
param k = do
val <- lookup k <$> getParams <$> ask
case val of
Nothing -> raise $ mconcat ["Param: ", k, " not found!"]
Just v -> either (const next) return $ parseParam v
class Parsable a where
parseParam :: T.Text -> Either T.Text a
parseParamList :: T.Text -> Either T.Text [a]
parseParamList t = mapM parseParam (T.split (== ',') t)
instance Parsable T.Text where parseParam = Right
instance Parsable B.ByteString where parseParam = Right . lazyTextToStrictByteString
instance Parsable Char where
parseParam t = case T.unpack t of
[c] -> Right c
_ -> Left "parseParam Char: no parse"
parseParamList = Right . T.unpack
instance Parsable () where
parseParam t = if T.null t then Right () else Left "parseParam Unit: no parse"
instance (Parsable a) => Parsable [a] where parseParam = parseParamList
instance Parsable Bool where parseParam = readEither
instance Parsable Double where parseParam = readEither
instance Parsable Float where parseParam = readEither
instance Parsable Int where parseParam = readEither
instance Parsable Integer where parseParam = readEither
readEither :: (Read a) => T.Text -> Either T.Text a
readEither t = case [ x | (x,"") <- reads (T.unpack t) ] of
[x] -> Right x
[] -> Left "readEither: no parse"
_ -> Left "readEither: ambiguous parse"
status :: Status -> ActionM ()
status = MS.modify . setStatus
header :: T.Text -> T.Text -> ActionM ()
header k v = MS.modify $ setHeader (CI.mk $ lazyTextToStrictByteString k, lazyTextToStrictByteString v)
text :: T.Text -> ActionM ()
text t = do
header "Content-Type" "text/plain"
MS.modify $ setContent $ ContentBuilder $ fromLazyByteString $ encodeUtf8 t
html :: T.Text -> ActionM ()
html t = do
header "Content-Type" "text/html"
MS.modify $ setContent $ ContentBuilder $ fromLazyByteString $ encodeUtf8 t
file :: FilePath -> ActionM ()
file = MS.modify . setContent . ContentFile
json :: (A.ToJSON a) => a -> ActionM ()
json v = do
header "Content-Type" "application/json"
MS.modify $ setContent $ ContentBuilder $ fromLazyByteString $ A.encode v
source :: Source (ResourceT IO) (Flush Builder) -> ActionM ()
source = MS.modify . setContent . ContentSource