-- | Twain is a tiny web application framework for WAI
--
-- - `ResponderM` for composing responses with do notation.
-- - Routing with path captures that decompose `ResponderM` into middleware.
-- - Parameter parsing for cookies, path, query, and body.
-- - Helpers for redirects, headers, status codes, and errors.
--
-- @
-- {-# language OverloadedStrings #-}
--
-- import Network.Wai.Handler.Warp (run)
-- import Web.Twain
--
-- main :: IO ()
-- main = do
--   run 8080 $
--     foldr ($)
--       (notFound missing)
--       [ get "/" index
--       , post "/echo/:name" echo
--       ]
--
-- index :: ResponderM a
-- index = send $ html "Hello World!"
--
-- echo :: ResponderM a
-- echo = do
--   name <- param "name"
--   send $ html $ "Hello, " <> name
--
-- missing :: ResponderM a
-- missing = send $ html "Not found..."
-- @
module Web.Twain
  ( ResponderM,

    -- * Routing
    get,
    put,
    patch,
    post,
    delete,
    route,
    notFound,

    -- * Requests
    param,
    paramEither,
    paramMaybe,
    params,
    file,
    fileMaybe,
    files,
    fromBody,
    header,
    headers,
    request,

    -- * Responses
    send,
    next,
    redirect301,
    redirect302,
    redirect303,
    text,
    html,
    json,
    xml,
    css,
    raw,
    status,
    withHeader,
    withCookie,
    withCookie',
    expireCookie,

    -- * Errors
    HttpError (..),
    onException,

    -- * Middleware
    withParseBodyOpts,
    withMaxBodySize,

    -- * Parameters
    ParsableParam (..),

    -- * Re-exports
    module Network.HTTP.Types,
    module Network.Wai,
    FileInfo (..),
  )
where

import Control.Exception (SomeException, handle)
import Control.Monad.Catch (throwM)
import Data.Aeson (ToJSON)
import qualified Data.Aeson as JSON
import Data.ByteString.Char8 as Char8
import Data.ByteString.Lazy as BL
import qualified Data.CaseInsensitive as CI
import Data.Either.Combinators (rightToMaybe)
import qualified Data.List as L
import Data.Maybe (fromMaybe)
import Data.Text as T
import Data.Text.Encoding
import Data.Time
import qualified Data.Vault.Lazy as V
import Data.Word (Word64)
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Parse hiding (Param)
import Network.Wai.Request
import System.Environment (lookupEnv)
import Web.Cookie
import Web.Twain.Internal
import Web.Twain.Types

get :: PathPattern -> ResponderM a -> Middleware
get :: PathPattern -> ResponderM a -> Middleware
get = Maybe Method -> PathPattern -> ResponderM a -> Middleware
forall a. Maybe Method -> PathPattern -> ResponderM a -> Middleware
route (Method -> Maybe Method
forall a. a -> Maybe a
Just Method
"GET")

put :: PathPattern -> ResponderM a -> Middleware
put :: PathPattern -> ResponderM a -> Middleware
put = Maybe Method -> PathPattern -> ResponderM a -> Middleware
forall a. Maybe Method -> PathPattern -> ResponderM a -> Middleware
route (Method -> Maybe Method
forall a. a -> Maybe a
Just Method
"PUT")

patch :: PathPattern -> ResponderM a -> Middleware
patch :: PathPattern -> ResponderM a -> Middleware
patch = Maybe Method -> PathPattern -> ResponderM a -> Middleware
forall a. Maybe Method -> PathPattern -> ResponderM a -> Middleware
route (Method -> Maybe Method
forall a. a -> Maybe a
Just Method
"PATCH")

post :: PathPattern -> ResponderM a -> Middleware
post :: PathPattern -> ResponderM a -> Middleware
post = Maybe Method -> PathPattern -> ResponderM a -> Middleware
forall a. Maybe Method -> PathPattern -> ResponderM a -> Middleware
route (Method -> Maybe Method
forall a. a -> Maybe a
Just Method
"POST")

delete :: PathPattern -> ResponderM a -> Middleware
delete :: PathPattern -> ResponderM a -> Middleware
delete = Maybe Method -> PathPattern -> ResponderM a -> Middleware
forall a. Maybe Method -> PathPattern -> ResponderM a -> Middleware
route (Method -> Maybe Method
forall a. a -> Maybe a
Just Method
"DELETE")

-- | Route request matching optional `Method` and `PathPattern` to `ResponderM`.
route :: Maybe Method -> PathPattern -> ResponderM a -> Middleware
route :: Maybe Method -> PathPattern -> ResponderM a -> Middleware
route Maybe Method
method PathPattern
pat (ResponderM Request -> IO (Either RouteAction (a, Request))
responder) Application
app Request
req Response -> IO ResponseReceived
respond = do
  let maxM :: Maybe Word64
maxM = ResponderOptions -> Word64
optsMaxBodySize (ResponderOptions -> Word64)
-> Maybe ResponderOptions -> Maybe Word64
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Key ResponderOptions -> Vault -> Maybe ResponderOptions
forall a. Key a -> Vault -> Maybe a
V.lookup Key ResponderOptions
responderOptsKey (Request -> Vault
vault Request
req)
  Request
req' <- IO Request -> (Word64 -> IO Request) -> Maybe Word64 -> IO Request
forall b a. b -> (a -> b) -> Maybe a -> b
maybe (Request -> IO Request
forall (f :: * -> *) a. Applicative f => a -> f a
pure Request
req) ((Word64 -> Request -> IO Request)
-> Request -> Word64 -> IO Request
forall a b c. (a -> b -> c) -> b -> a -> c
flip Word64 -> Request -> IO Request
requestSizeCheck Request
req) Maybe Word64
maxM
  case Maybe Method -> PathPattern -> Request -> Maybe [Param]
match Maybe Method
method PathPattern
pat Request
req' of
    Maybe [Param]
Nothing -> Application
app Request
req' Response -> IO ResponseReceived
respond
    Just [Param]
pathParams -> do
      let preq :: ParsedRequest
preq = Request -> ParsedRequest
parseRequest Request
req'
          preq' :: ParsedRequest
preq' = ParsedRequest
preq {preqPathParams :: [Param]
preqPathParams = [Param]
pathParams}
          req'' :: Request
req'' = Request
req' {vault :: Vault
vault = Key ParsedRequest -> ParsedRequest -> Vault -> Vault
forall a. Key a -> a -> Vault -> Vault
V.insert Key ParsedRequest
parsedReqKey ParsedRequest
preq' (Request -> Vault
vault Request
req')}
      Either RouteAction (a, Request)
eres <- Request -> IO (Either RouteAction (a, Request))
responder Request
req''
      case Either RouteAction (a, Request)
eres of
        Left (Respond Response
res) -> Response -> IO ResponseReceived
respond Response
res
        Either RouteAction (a, Request)
_ -> Application
app Request
req'' Response -> IO ResponseReceived
respond

-- | Respond if no other route responds.
--
-- Sets the status to 404.
notFound :: ResponderM a -> Application
notFound :: ResponderM a -> Application
notFound (ResponderM Request -> IO (Either RouteAction (a, Request))
responder) Request
req Response -> IO ResponseReceived
respond = do
  let preq :: ParsedRequest
preq = Request -> ParsedRequest
parseRequest Request
req
      req' :: Request
req' = Request
req {vault :: Vault
vault = Key ParsedRequest -> ParsedRequest -> Vault -> Vault
forall a. Key a -> a -> Vault -> Vault
V.insert Key ParsedRequest
parsedReqKey ParsedRequest
preq (Request -> Vault
vault Request
req)}
  Either RouteAction (a, Request)
eres <- Request -> IO (Either RouteAction (a, Request))
responder Request
req'
  case Either RouteAction (a, Request)
eres of
    Left (Respond Response
res) -> Response -> IO ResponseReceived
respond (Response -> IO ResponseReceived)
-> Response -> IO ResponseReceived
forall a b. (a -> b) -> a -> b
$ (Status -> Status) -> Response -> Response
mapResponseStatus (Status -> Status -> Status
forall a b. a -> b -> a
const Status
status404) Response
res
    Either RouteAction (a, Request)
_ -> Response -> IO ResponseReceived
respond (Response -> IO ResponseReceived)
-> Response -> IO ResponseReceived
forall a b. (a -> b) -> a -> b
$ Status -> Response -> Response
status Status
status404 (Response -> Response) -> Response -> Response
forall a b. (a -> b) -> a -> b
$ Text -> Response
text Text
"Not found."

onException :: (SomeException -> ResponderM a) -> Middleware
onException :: (SomeException -> ResponderM a) -> Middleware
onException SomeException -> ResponderM a
h Application
app Request
req Response -> IO ResponseReceived
respond = do
  (SomeException -> IO ResponseReceived)
-> IO ResponseReceived -> IO ResponseReceived
forall e a. Exception e => (e -> IO a) -> IO a -> IO a
handle SomeException -> IO ResponseReceived
handler (IO ResponseReceived -> IO ResponseReceived)
-> IO ResponseReceived -> IO ResponseReceived
forall a b. (a -> b) -> a -> b
$ Application
app Request
req Response -> IO ResponseReceived
respond
  where
    handler :: SomeException -> IO ResponseReceived
handler SomeException
err = do
      let preq :: ParsedRequest
preq = Request -> ParsedRequest
parseRequest Request
req
          req' :: Request
req' = Request
req {vault :: Vault
vault = Key ParsedRequest -> ParsedRequest -> Vault -> Vault
forall a. Key a -> a -> Vault -> Vault
V.insert Key ParsedRequest
parsedReqKey ParsedRequest
preq (Request -> Vault
vault Request
req)}
      let (ResponderM Request -> IO (Either RouteAction (a, Request))
responder) = SomeException -> ResponderM a
h SomeException
err
      Either RouteAction (a, Request)
eres <- Request -> IO (Either RouteAction (a, Request))
responder Request
req'
      case Either RouteAction (a, Request)
eres of
        Left (Respond Response
res) -> Response -> IO ResponseReceived
respond Response
res
        Either RouteAction (a, Request)
_ -> Application
app Request
req' Response -> IO ResponseReceived
respond

-- | Specify maximum request body size in bytes.
--
-- Defaults to 64KB.
withMaxBodySize :: Word64 -> Middleware
withMaxBodySize :: Word64 -> Middleware
withMaxBodySize Word64
max Application
app Request
req Response -> IO ResponseReceived
respond = do
  let optsM :: Maybe ResponderOptions
optsM = Key ResponderOptions -> Vault -> Maybe ResponderOptions
forall a. Key a -> Vault -> Maybe a
V.lookup Key ResponderOptions
responderOptsKey (Request -> Vault
vault Request
req)
      opts :: ResponderOptions
opts = ResponderOptions -> Maybe ResponderOptions -> ResponderOptions
forall a. a -> Maybe a -> a
fromMaybe ResponderOptions
defaultResponderOpts Maybe ResponderOptions
optsM
      opts' :: ResponderOptions
opts' = ResponderOptions
opts {optsMaxBodySize :: Word64
optsMaxBodySize = Word64
max}
  let req' :: Request
req' = Request
req {vault :: Vault
vault = Key ResponderOptions -> ResponderOptions -> Vault -> Vault
forall a. Key a -> a -> Vault -> Vault
V.insert Key ResponderOptions
responderOptsKey ResponderOptions
opts' (Request -> Vault
vault Request
req)}
  Application
app Request
req' Response -> IO ResponseReceived
respond

-- | Specify `ParseRequestBodyOptions` to use when parsing request body.
withParseBodyOpts :: ParseRequestBodyOptions -> Middleware
withParseBodyOpts :: ParseRequestBodyOptions -> Middleware
withParseBodyOpts ParseRequestBodyOptions
parseBodyOpts Application
app Request
req Response -> IO ResponseReceived
respond = do
  let optsM :: Maybe ResponderOptions
optsM = Key ResponderOptions -> Vault -> Maybe ResponderOptions
forall a. Key a -> Vault -> Maybe a
V.lookup Key ResponderOptions
responderOptsKey (Request -> Vault
vault Request
req)
      opts :: ResponderOptions
opts = ResponderOptions -> Maybe ResponderOptions -> ResponderOptions
forall a. a -> Maybe a -> a
fromMaybe ResponderOptions
defaultResponderOpts Maybe ResponderOptions
optsM
      opts' :: ResponderOptions
opts' = ResponderOptions
opts {optsParseBody :: ParseRequestBodyOptions
optsParseBody = ParseRequestBodyOptions
parseBodyOpts}
  let req' :: Request
req' = Request
req {vault :: Vault
vault = Key ResponderOptions -> ResponderOptions -> Vault -> Vault
forall a. Key a -> a -> Vault -> Vault
V.insert Key ResponderOptions
responderOptsKey ResponderOptions
opts' (Request -> Vault
vault Request
req)}
  Application
app Request
req' Response -> IO ResponseReceived
respond

-- | Get a parameter. Looks in query, path, cookie, and body (in that order).
--
-- If no parameter is found, or parameter fails to parse, `next` is called
-- which passes control to subsequent routes and middleware.
param :: ParsableParam a => Text -> ResponderM a
param :: Text -> ResponderM a
param Text
name = do
  Maybe Text
pM <- (Param -> Text) -> Maybe Param -> Maybe Text
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Param -> Text
forall a b. (a, b) -> b
snd (Maybe Param -> Maybe Text)
-> ([Param] -> Maybe Param) -> [Param] -> Maybe Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Param -> Bool) -> [Param] -> Maybe Param
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
L.find (Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
(==) Text
name (Text -> Bool) -> (Param -> Text) -> Param -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Param -> Text
forall a b. (a, b) -> a
fst) ([Param] -> Maybe Text)
-> ResponderM [Param] -> ResponderM (Maybe Text)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ResponderM [Param]
params
  ResponderM a
-> (Text -> ResponderM a) -> Maybe Text -> ResponderM a
forall b a. b -> (a -> b) -> Maybe a -> b
maybe ResponderM a
forall a. ResponderM a
next ((HttpError -> ResponderM a)
-> (a -> ResponderM a) -> Either HttpError a -> ResponderM a
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either (ResponderM a -> HttpError -> ResponderM a
forall a b. a -> b -> a
const ResponderM a
forall a. ResponderM a
next) a -> ResponderM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Either HttpError a -> ResponderM a)
-> (Text -> Either HttpError a) -> Text -> ResponderM a
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Either HttpError a
forall a. ParsableParam a => Text -> Either HttpError a
parseParam) Maybe Text
pM

-- | Get a parameter or error if missing or parse failure.
paramEither :: ParsableParam a => Text -> ResponderM (Either HttpError a)
paramEither :: Text -> ResponderM (Either HttpError a)
paramEither Text
name = do
  Maybe Text
pM <- (Param -> Text) -> Maybe Param -> Maybe Text
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Param -> Text
forall a b. (a, b) -> b
snd (Maybe Param -> Maybe Text)
-> ([Param] -> Maybe Param) -> [Param] -> Maybe Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Param -> Bool) -> [Param] -> Maybe Param
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
L.find (Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
(==) Text
name (Text -> Bool) -> (Param -> Text) -> Param -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Param -> Text
forall a b. (a, b) -> a
fst) ([Param] -> Maybe Text)
-> ResponderM [Param] -> ResponderM (Maybe Text)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ResponderM [Param]
params
  Either HttpError a -> ResponderM (Either HttpError a)
forall (m :: * -> *) a. Monad m => a -> m a
return (Either HttpError a -> ResponderM (Either HttpError a))
-> Either HttpError a -> ResponderM (Either HttpError a)
forall a b. (a -> b) -> a -> b
$ case Maybe Text
pM of
    Maybe Text
Nothing ->
      HttpError -> Either HttpError a
forall a b. a -> Either a b
Left (HttpError -> Either HttpError a)
-> HttpError -> Either HttpError a
forall a b. (a -> b) -> a -> b
$ Status -> String -> HttpError
HttpError Status
status400 (String
"missing parameter: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> Text -> String
T.unpack Text
name)
    Just Text
p -> Text -> Either HttpError a
forall a. ParsableParam a => Text -> Either HttpError a
parseParam Text
p

-- | Get an optional parameter.
--
-- Returns `Nothing` for missing parameter.
-- Throws `HttpError` on parse failure.
paramMaybe :: ParsableParam a => Text -> ResponderM (Maybe a)
paramMaybe :: Text -> ResponderM (Maybe a)
paramMaybe Text
name = do
  Maybe Text
pM <- (Param -> Text) -> Maybe Param -> Maybe Text
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Param -> Text
forall a b. (a, b) -> b
snd (Maybe Param -> Maybe Text)
-> ([Param] -> Maybe Param) -> [Param] -> Maybe Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Param -> Bool) -> [Param] -> Maybe Param
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
L.find (Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
(==) Text
name (Text -> Bool) -> (Param -> Text) -> Param -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Param -> Text
forall a b. (a, b) -> a
fst) ([Param] -> Maybe Text)
-> ResponderM [Param] -> ResponderM (Maybe Text)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ResponderM [Param]
params
  Maybe a -> ResponderM (Maybe a)
forall (m :: * -> *) a. Monad m => a -> m a
return (Maybe a -> ResponderM (Maybe a))
-> Maybe a -> ResponderM (Maybe a)
forall a b. (a -> b) -> a -> b
$ Maybe a -> (Text -> Maybe a) -> Maybe Text -> Maybe a
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Maybe a
forall a. Maybe a
Nothing (Either HttpError a -> Maybe a
forall a b. Either a b -> Maybe b
rightToMaybe (Either HttpError a -> Maybe a)
-> (Text -> Either HttpError a) -> Text -> Maybe a
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Either HttpError a
forall a. ParsableParam a => Text -> Either HttpError a
parseParam) Maybe Text
pM

-- | Get all parameters from query, path, cookie, and body (in that order).
params :: ResponderM [Param]
params :: ResponderM [Param]
params = ParsedRequest -> [Param]
concatParams (ParsedRequest -> [Param])
-> ResponderM ParsedRequest -> ResponderM [Param]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ResponderM ParsedRequest
parseBodyForm

-- | Get uploaded `FileInfo`.
--
-- If missing parameter or empty file, pass control to subsequent routes and
-- middleware.
file :: Text -> ResponderM (FileInfo BL.ByteString)
file :: Text -> ResponderM (FileInfo ByteString)
file Text
name = ResponderM (FileInfo ByteString)
-> (FileInfo ByteString -> ResponderM (FileInfo ByteString))
-> Maybe (FileInfo ByteString)
-> ResponderM (FileInfo ByteString)
forall b a. b -> (a -> b) -> Maybe a -> b
maybe ResponderM (FileInfo ByteString)
forall a. ResponderM a
next FileInfo ByteString -> ResponderM (FileInfo ByteString)
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Maybe (FileInfo ByteString) -> ResponderM (FileInfo ByteString))
-> ResponderM (Maybe (FileInfo ByteString))
-> ResponderM (FileInfo ByteString)
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< Text -> ResponderM (Maybe (FileInfo ByteString))
fileMaybe Text
name

-- | Get optional uploaded `FileInfo`.
--
-- `Nothing` is returned for missing parameter or empty file content.
fileMaybe :: Text -> ResponderM (Maybe (FileInfo BL.ByteString))
fileMaybe :: Text -> ResponderM (Maybe (FileInfo ByteString))
fileMaybe Text
name = do
  Maybe (FileInfo ByteString)
fM <- ((Method, FileInfo ByteString) -> FileInfo ByteString)
-> Maybe (Method, FileInfo ByteString)
-> Maybe (FileInfo ByteString)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Method, FileInfo ByteString) -> FileInfo ByteString
forall a b. (a, b) -> b
snd (Maybe (Method, FileInfo ByteString)
 -> Maybe (FileInfo ByteString))
-> ([(Method, FileInfo ByteString)]
    -> Maybe (Method, FileInfo ByteString))
-> [(Method, FileInfo ByteString)]
-> Maybe (FileInfo ByteString)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((Method, FileInfo ByteString) -> Bool)
-> [(Method, FileInfo ByteString)]
-> Maybe (Method, FileInfo ByteString)
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
L.find (Method -> Method -> Bool
forall a. Eq a => a -> a -> Bool
(==) (Text -> Method
encodeUtf8 Text
name) (Method -> Bool)
-> ((Method, FileInfo ByteString) -> Method)
-> (Method, FileInfo ByteString)
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Method, FileInfo ByteString) -> Method
forall a b. (a, b) -> a
fst) ([(Method, FileInfo ByteString)] -> Maybe (FileInfo ByteString))
-> ResponderM [(Method, FileInfo ByteString)]
-> ResponderM (Maybe (FileInfo ByteString))
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ResponderM [(Method, FileInfo ByteString)]
files
  case FileInfo ByteString -> ByteString
forall c. FileInfo c -> c
fileContent (FileInfo ByteString -> ByteString)
-> Maybe (FileInfo ByteString) -> Maybe ByteString
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe (FileInfo ByteString)
fM of
    Maybe ByteString
Nothing -> Maybe (FileInfo ByteString)
-> ResponderM (Maybe (FileInfo ByteString))
forall (m :: * -> *) a. Monad m => a -> m a
return Maybe (FileInfo ByteString)
forall a. Maybe a
Nothing
    Just ByteString
"" -> Maybe (FileInfo ByteString)
-> ResponderM (Maybe (FileInfo ByteString))
forall (m :: * -> *) a. Monad m => a -> m a
return Maybe (FileInfo ByteString)
forall a. Maybe a
Nothing
    Just ByteString
_ -> Maybe (FileInfo ByteString)
-> ResponderM (Maybe (FileInfo ByteString))
forall (m :: * -> *) a. Monad m => a -> m a
return Maybe (FileInfo ByteString)
fM

-- | Get all uploaded files.
files :: ResponderM [File BL.ByteString]
files :: ResponderM [(Method, FileInfo ByteString)]
files = Maybe ParsedBody -> [(Method, FileInfo ByteString)]
fs (Maybe ParsedBody -> [(Method, FileInfo ByteString)])
-> (ParsedRequest -> Maybe ParsedBody)
-> ParsedRequest
-> [(Method, FileInfo ByteString)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ParsedRequest -> Maybe ParsedBody
preqBody (ParsedRequest -> [(Method, FileInfo ByteString)])
-> ResponderM ParsedRequest
-> ResponderM [(Method, FileInfo ByteString)]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ResponderM ParsedRequest
parseBodyForm
  where
    fs :: Maybe ParsedBody -> [(Method, FileInfo ByteString)]
fs Maybe ParsedBody
bodyM = case Maybe ParsedBody
bodyM of
      Just (FormBody ([Param]
_, [(Method, FileInfo ByteString)]
fs)) -> [(Method, FileInfo ByteString)]
fs
      Maybe ParsedBody
_ -> []

-- | Get the JSON value from request body.
fromBody :: JSON.FromJSON a => ResponderM a
fromBody :: ResponderM a
fromBody = do
  Value
json <- ResponderM Value
parseBodyJson
  case Value -> Result a
forall a. FromJSON a => Value -> Result a
JSON.fromJSON Value
json of
    JSON.Error String
msg -> HttpError -> ResponderM a
forall (m :: * -> *) e a. (MonadThrow m, Exception e) => e -> m a
throwM (HttpError -> ResponderM a) -> HttpError -> ResponderM a
forall a b. (a -> b) -> a -> b
$ Status -> String -> HttpError
HttpError Status
status400 String
msg
    JSON.Success a
a -> a -> ResponderM a
forall (m :: * -> *) a. Monad m => a -> m a
return a
a

-- | Get the value of a request `Header`. Header names are case-insensitive.
header :: Text -> ResponderM (Maybe Text)
header :: Text -> ResponderM (Maybe Text)
header Text
name = do
  let ciname :: CI Method
ciname = Method -> CI Method
forall s. FoldCase s => s -> CI s
CI.mk (Text -> Method
encodeUtf8 Text
name)
  ((CI Method, Method) -> Text)
-> Maybe (CI Method, Method) -> Maybe Text
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Method -> Text
decodeUtf8 (Method -> Text)
-> ((CI Method, Method) -> Method) -> (CI Method, Method) -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (CI Method, Method) -> Method
forall a b. (a, b) -> b
snd) (Maybe (CI Method, Method) -> Maybe Text)
-> ([(CI Method, Method)] -> Maybe (CI Method, Method))
-> [(CI Method, Method)]
-> Maybe Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((CI Method, Method) -> Bool)
-> [(CI Method, Method)] -> Maybe (CI Method, Method)
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
L.find (CI Method -> CI Method -> Bool
forall a. Eq a => a -> a -> Bool
(==) CI Method
ciname (CI Method -> Bool)
-> ((CI Method, Method) -> CI Method)
-> (CI Method, Method)
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (CI Method, Method) -> CI Method
forall a b. (a, b) -> a
fst) ([(CI Method, Method)] -> Maybe Text)
-> ResponderM [(CI Method, Method)] -> ResponderM (Maybe Text)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ResponderM [(CI Method, Method)]
headers

-- | Get the request headers.
headers :: ResponderM [Header]
headers :: ResponderM [(CI Method, Method)]
headers = Request -> [(CI Method, Method)]
requestHeaders (Request -> [(CI Method, Method)])
-> ResponderM Request -> ResponderM [(CI Method, Method)]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ResponderM Request
request

-- | Get the WAI `Request`.
request :: ResponderM Request
request :: ResponderM Request
request = ResponderM Request
getRequest

-- | Send a `Response`.
--
-- > send $ text "Hello, World!"
--
-- Send an `html` response:
--
-- > send $ html "<h1>Hello, World!</h1>"
--
-- Modify the `status`:
--
-- > send $ status status404 $ text "Not Found"
--
-- Send a response `withHeader`:
--
-- > send $ withHeader (hServer, "Twain + Warp") $ text "Hello"
--
-- Send a response `withCookie`:
--
-- > send $ withCookie "key" "val" $ text "Hello"
send :: Response -> ResponderM a
send :: Response -> ResponderM a
send Response
res = (Request -> IO (Either RouteAction (a, Request))) -> ResponderM a
forall a.
(Request -> IO (Either RouteAction (a, Request))) -> ResponderM a
ResponderM ((Request -> IO (Either RouteAction (a, Request))) -> ResponderM a)
-> (Request -> IO (Either RouteAction (a, Request)))
-> ResponderM a
forall a b. (a -> b) -> a -> b
$ \Request
_ -> Either RouteAction (a, Request)
-> IO (Either RouteAction (a, Request))
forall (m :: * -> *) a. Monad m => a -> m a
return (Either RouteAction (a, Request)
 -> IO (Either RouteAction (a, Request)))
-> Either RouteAction (a, Request)
-> IO (Either RouteAction (a, Request))
forall a b. (a -> b) -> a -> b
$ RouteAction -> Either RouteAction (a, Request)
forall a b. a -> Either a b
Left (Response -> RouteAction
Respond Response
res)

-- | Pass control to the next route or middleware.
next :: ResponderM a
next :: ResponderM a
next = (Request -> IO (Either RouteAction (a, Request))) -> ResponderM a
forall a.
(Request -> IO (Either RouteAction (a, Request))) -> ResponderM a
ResponderM ((Request -> IO (Either RouteAction (a, Request))) -> ResponderM a)
-> (Request -> IO (Either RouteAction (a, Request)))
-> ResponderM a
forall a b. (a -> b) -> a -> b
$ \Request
_ -> Either RouteAction (a, Request)
-> IO (Either RouteAction (a, Request))
forall (m :: * -> *) a. Monad m => a -> m a
return (RouteAction -> Either RouteAction (a, Request)
forall a b. a -> Either a b
Left RouteAction
Next)

-- | Construct a `Text` response.
--
-- Sets the Content-Type and Content-Length headers.
text :: Text -> Response
text :: Text -> Response
text =
   Status -> [(CI Method, Method)] -> ByteString -> Response
raw Status
status200 [(CI Method
hContentType, Method
"text/plain; charset=utf-8")] (ByteString -> Response)
-> (Text -> ByteString) -> Text -> Response
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Method -> ByteString
BL.fromStrict (Method -> ByteString) -> (Text -> Method) -> Text -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Method
encodeUtf8

-- | Construct an HTML response.
--
-- Sets the Content-Type and Content-Length headers.
html :: BL.ByteString -> Response
html :: ByteString -> Response
html =
   Status -> [(CI Method, Method)] -> ByteString -> Response
raw Status
status200 [(CI Method
hContentType, Method
"text/html; charset=utf-8")]

-- | Construct a JSON response using `ToJSON`.
--
-- Sets the Content-Type and Content-Length headers.
json :: ToJSON a => a -> Response
json :: a -> Response
json =
  Status -> [(CI Method, Method)] -> ByteString -> Response
raw Status
status200 [(CI Method
hContentType, Method
"application/json; charset=utf-8")] (ByteString -> Response) -> (a -> ByteString) -> a -> Response
forall b c a. (b -> c) -> (a -> b) -> a -> c
. a -> ByteString
forall a. ToJSON a => a -> ByteString
JSON.encode

-- | Construct a CSS response.
--
-- Sets the Content-Type and Content-Length headers.
css :: BL.ByteString -> Response
css :: ByteString -> Response
css =
  Status -> [(CI Method, Method)] -> ByteString -> Response
raw Status
status200 [(CI Method
hContentType, Method
"text/css; charset=utf-8")]

-- | Construct an XML response.
--
-- Sets the Content-Type and Content-Length headers.
xml :: BL.ByteString -> Response
xml :: ByteString -> Response
xml = Status -> [(CI Method, Method)] -> ByteString -> Response
raw Status
status200 [(CI Method
hContentType, Method
"application/xml; charset=utf-8")]

-- | Construct a raw response from a lazy `ByteString`.
--
-- Sets the Content-Length header if missing.
raw :: Status -> [Header] -> BL.ByteString -> Response
raw :: Status -> [(CI Method, Method)] -> ByteString -> Response
raw Status
status [(CI Method, Method)]
headers ByteString
body =
  if ((CI Method, Method) -> Bool) -> [(CI Method, Method)] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
L.any ((CI Method
hContentLength CI Method -> CI Method -> Bool
forall a. Eq a => a -> a -> Bool
==) (CI Method -> Bool)
-> ((CI Method, Method) -> CI Method)
-> (CI Method, Method)
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (CI Method, Method) -> CI Method
forall a b. (a, b) -> a
fst) [(CI Method, Method)]
headers
    then Status -> [(CI Method, Method)] -> ByteString -> Response
responseLBS Status
status [(CI Method, Method)]
headers ByteString
body
    else
      let len :: (CI Method, Method)
len = (CI Method
hContentLength, String -> Method
Char8.pack (Int64 -> String
forall a. Show a => a -> String
show (ByteString -> Int64
BL.length ByteString
body)))
       in Status -> [(CI Method, Method)] -> ByteString -> Response
responseLBS Status
status ((CI Method, Method)
len (CI Method, Method)
-> [(CI Method, Method)] -> [(CI Method, Method)]
forall a. a -> [a] -> [a]
: [(CI Method, Method)]
headers) ByteString
body

-- | Set the `Status` for a `Response`.
status :: Status -> Response -> Response
status :: Status -> Response -> Response
status Status
s = (Status -> Status) -> Response -> Response
mapResponseStatus (Status -> Status -> Status
forall a b. a -> b -> a
const Status
s)

-- | Add a `Header` to response.
withHeader :: Header -> Response -> Response
withHeader :: (CI Method, Method) -> Response -> Response
withHeader (CI Method, Method)
header = ([(CI Method, Method)] -> [(CI Method, Method)])
-> Response -> Response
mapResponseHeaders ((CI Method, Method)
header (CI Method, Method)
-> [(CI Method, Method)] -> [(CI Method, Method)]
forall a. a -> [a] -> [a]
:)

-- | Add a cookie to the response with the given key and value.
--
-- Note: This uses `defaultSetCookie`.
withCookie :: Text -> Text -> Response -> Response
withCookie :: Text -> Text -> Response -> Response
withCookie Text
key Text
val Response
res =
  let setCookie :: SetCookie
setCookie =
        SetCookie
defaultSetCookie
          { setCookieName :: Method
setCookieName = Text -> Method
encodeUtf8 Text
key,
            setCookieValue :: Method
setCookieValue = Text -> Method
encodeUtf8 Text
val,
            setCookiePath :: Maybe Method
setCookiePath = Method -> Maybe Method
forall a. a -> Maybe a
Just Method
"/",
            setCookieHttpOnly :: Bool
setCookieHttpOnly = Bool
True
          }
      header :: (CI Method, Method)
header = (Method -> CI Method
forall s. FoldCase s => s -> CI s
CI.mk Method
"Set-Cookie", SetCookie -> Method
setCookieByteString SetCookie
setCookie)
   in ([(CI Method, Method)] -> [(CI Method, Method)])
-> Response -> Response
mapResponseHeaders ((CI Method, Method)
header (CI Method, Method)
-> [(CI Method, Method)] -> [(CI Method, Method)]
forall a. a -> [a] -> [a]
:) Response
res

-- | Add a `SetCookie` to the response.
withCookie' :: SetCookie -> Response -> Response
withCookie' :: SetCookie -> Response -> Response
withCookie' SetCookie
setCookie Response
res =
  let header :: (CI Method, Method)
header = (Method -> CI Method
forall s. FoldCase s => s -> CI s
CI.mk Method
"Set-Cookie", SetCookie -> Method
setCookieByteString SetCookie
setCookie)
   in ([(CI Method, Method)] -> [(CI Method, Method)])
-> Response -> Response
mapResponseHeaders ((CI Method, Method)
header (CI Method, Method)
-> [(CI Method, Method)] -> [(CI Method, Method)]
forall a. a -> [a] -> [a]
:) Response
res

-- | Add a header to expire (unset) a cookie with the given key.
expireCookie :: Text -> Response -> Response
expireCookie :: Text -> Response -> Response
expireCookie Text
key Response
res = do
  let zeroTime :: UTCTime
zeroTime = Day -> DiffTime -> UTCTime
UTCTime (Integer -> Day
ModifiedJulianDay Integer
0) (Integer -> DiffTime
secondsToDiffTime Integer
0)
      setCookie :: SetCookie
setCookie =
        SetCookie
defaultSetCookie
          { setCookieName :: Method
setCookieName = Text -> Method
encodeUtf8 Text
key,
            setCookiePath :: Maybe Method
setCookiePath = Method -> Maybe Method
forall a. a -> Maybe a
Just Method
"/",
            setCookieHttpOnly :: Bool
setCookieHttpOnly = Bool
True,
            setCookieExpires :: Maybe UTCTime
setCookieExpires = UTCTime -> Maybe UTCTime
forall a. a -> Maybe a
Just UTCTime
zeroTime
          }
      header :: (CI Method, Method)
header = (Method -> CI Method
forall s. FoldCase s => s -> CI s
CI.mk Method
"Set-Cookie", SetCookie -> Method
setCookieByteString SetCookie
setCookie)
   in ([(CI Method, Method)] -> [(CI Method, Method)])
-> Response -> Response
mapResponseHeaders ((CI Method, Method)
header (CI Method, Method)
-> [(CI Method, Method)] -> [(CI Method, Method)]
forall a. a -> [a] -> [a]
:) Response
res

-- | Create a redirect response with 301 status (Moved Permanently).
redirect301 :: Text -> Response
redirect301 :: Text -> Response
redirect301 Text
url = Status -> [(CI Method, Method)] -> ByteString -> Response
raw Status
status301 [(CI Method
hLocation, Text -> Method
encodeUtf8 Text
url)] ByteString
""

-- | Create a redirect response with 302 status (Found).
redirect302 :: Text -> Response
redirect302 :: Text -> Response
redirect302 Text
url = Status -> [(CI Method, Method)] -> ByteString -> Response
raw Status
status302 [(CI Method
hLocation, Text -> Method
encodeUtf8 Text
url)] ByteString
""

-- | Create a redirect response 303 status (See Other).
redirect303 :: Text -> Response
redirect303 :: Text -> Response
redirect303 Text
url = Status -> [(CI Method, Method)] -> ByteString -> Response
raw Status
status303 [(CI Method
hLocation, Text -> Method
encodeUtf8 Text
url)] ByteString
""