twain-1.0.0.0: src/Web/Twain/Types.hs
module Web.Twain.Types where
import Control.Exception (SomeException)
import Control.Monad (ap)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson as JSON
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.Either.Combinators (mapRight)
import Data.Int
import Data.List as L
import Data.String (IsString, fromString)
import Data.Text as T
import Data.Text.Encoding
import qualified Data.Text.Lazy as TL
import Data.Word
import Network.Wai (Middleware, Request, Response, pathInfo)
import Network.Wai.Handler.Warp (defaultOnExceptionResponse)
import Network.Wai.Parse (File, ParseRequestBodyOptions)
import Numeric.Natural
-- | TwainM provides a monad interface for composing routes and middleware.
newtype TwainM e a = TwainM (TwainState e -> (a, TwainState e))
data TwainState e
= TwainState
{ middlewares :: [Middleware],
environment :: e,
onExceptionResponse :: (SomeException -> Response)
}
instance Functor (TwainM e) where
fmap f (TwainM g) = TwainM $ \s ->
let (a, sb) = g s
in (f a, sb)
instance Applicative (TwainM e) where
pure = return
(<*>) = ap
instance Monad (TwainM e) where
return a = TwainM (\s -> (a, s))
(TwainM m) >>= fn = TwainM $ \s ->
let (a, sb) = m s
(TwainM mb) = fn a
in mb sb
modify :: (TwainState e -> TwainState e) -> TwainM e ()
modify f = TwainM (\s -> ((), f s))
exec :: TwainM e a -> e -> TwainState e
exec (TwainM f) e = snd (f (TwainState [] e defaultOnExceptionResponse))
-- | `RouteM` is a Reader-like monad that can "short-circuit" and return a WAI
-- response using a given environment. This provides convenient branching with
-- do notation for redirects, error responses, etc.
data RouteM e a
= RouteM (RouteState e -> IO (Either RouteAction (a, RouteState e)))
data RouteAction
= Respond Response
| Next
data RouteState e
= RouteState
{ reqBodyParams :: [Param],
reqBodyFiles :: [File BL.ByteString],
reqPathParams :: [Param],
reqQueryParams :: [Param],
reqCookieParams :: [Param],
reqBodyJson :: Either String JSON.Value,
reqBodyParsed :: Bool,
reqEnv :: e,
reqWai :: Request
}
instance Functor (RouteM e) where
fmap f (RouteM g) = RouteM $ \s -> mapRight (\(a, b) -> (f a, b)) `fmap` g s
instance Applicative (RouteM e) where
pure = return
(<*>) = ap
instance Monad (RouteM e) where
return a = RouteM $ \s -> return (Right (a, s))
(RouteM act) >>= fn = RouteM $ \s -> do
eres <- act s
case eres of
Left ract -> return (Left ract)
Right (a, sb) -> do
let (RouteM fres) = fn a
fres sb
instance MonadIO (RouteM e) where
liftIO act = RouteM $ \s -> act >>= \a -> return (Right (a, s))
type Param = (Text, Text)
data PathPattern = MatchPath (Request -> Maybe [Param])
instance IsString PathPattern where
fromString s = MatchPath (matchPath (T.pack s))
matchPath :: Text -> Request -> Maybe [Param]
matchPath path req =
go (splitPath path) (pathInfo req) (Just [])
where
splitPath = L.filter (not . T.null) . T.split (== '/')
go (p : ps) (r : rs) m@(Just pms) =
if not (T.null p) && T.head p == ':'
then go ps rs (Just ((T.drop 1 p, r) : pms))
else if p == r then go ps rs m else Nothing
go [] [] pms = pms
go _ _ _ = Nothing
-- | Parse values from request parameters.
class ParsableParam a where
parseParam :: Text -> Either Text a
-- | Default implementation parses comma-delimited lists.
parseParamList :: Text -> Either Text [a]
parseParamList t = mapM parseParam (T.split (== ',') t)
-- ParsableParam class and instance code is from Andrew Farmer and Scotty
-- framework, with slight modifications.
instance ParsableParam TL.Text where parseParam = Right . TL.fromStrict
instance ParsableParam T.Text where parseParam = Right
instance ParsableParam B.ByteString where parseParam = Right . encodeUtf8
instance ParsableParam BL.ByteString where parseParam = Right . BL.fromStrict . encodeUtf8
instance ParsableParam Char where
parseParam t = case T.unpack t of
[c] -> Right c
_ -> Left "parseParam Char: no parse"
parseParamList = Right . T.unpack -- String
instance ParsableParam () where
parseParam t = if T.null t then Right () else Left "parseParam Unit: no parse"
instance (ParsableParam a) => ParsableParam [a] where parseParam = parseParamList
instance ParsableParam Bool where
parseParam t =
if t' == T.toCaseFold "true"
then Right True
else
if t' == T.toCaseFold "false"
then Right False
else Left "parseParam Bool: no parse"
where
t' = T.toCaseFold t
instance ParsableParam Double where parseParam = readEither
instance ParsableParam Float where parseParam = readEither
instance ParsableParam Int where parseParam = readEither
instance ParsableParam Int8 where parseParam = readEither
instance ParsableParam Int16 where parseParam = readEither
instance ParsableParam Int32 where parseParam = readEither
instance ParsableParam Int64 where parseParam = readEither
instance ParsableParam Integer where parseParam = readEither
instance ParsableParam Word where parseParam = readEither
instance ParsableParam Word8 where parseParam = readEither
instance ParsableParam Word16 where parseParam = readEither
instance ParsableParam Word32 where parseParam = readEither
instance ParsableParam Word64 where parseParam = readEither
instance ParsableParam Natural where parseParam = readEither
-- | Useful for creating 'ParsableParam' instances for things that already implement 'Read'.
readEither :: Read a => Text -> Either Text a
readEither t = case [x | (x, "") <- reads (T.unpack t)] of
[x] -> Right x
[] -> Left "readEither: no parse"
_ -> Left "readEither: ambiguous parse"