{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Airship.Types
( ETag(..)
, Webmachine
, AirshipRequest(..)
, Request(..)
, RequestReader(..)
, Response(..)
, ResponseState(..)
, ResponseBody(..)
, ErrorResponses
, addTrace
, defaultRequest
, entireRequestBody
, etagToByteString
, eitherResponse
, escapedResponse
, mapWebmachine
, runWebmachine
, request
, requestTime
, routePath
, getResponseHeaders
, getResponseBody
, params
, dispatchPath
, putResponseBody
, putResponseBS
, halt
, finishWith
) where
import Airship.RST
import Blaze.ByteString.Builder (Builder)
import Blaze.ByteString.Builder.ByteString (fromByteString)
import Blaze.ByteString.Builder.Html.Utf8 (fromHtmlEscapedText)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LB
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative
#endif
import Control.Monad (liftM)
import Control.Monad.Base (MonadBase)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Morph
import Control.Monad.Reader.Class (MonadReader, ask)
import Control.Monad.State.Class
import Control.Monad.Trans.Control (MonadBaseControl (..))
import Data.ByteString.Char8 hiding (reverse)
import Data.HashMap.Strict (HashMap)
import Data.Map.Strict (Map)
import Data.Monoid ((<>))
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Network.HTTP.Media
import qualified Network.HTTP.Types as HTTP
import Network.HTTP.Types (ResponseHeaders, Status)
import Network.Wai (Request (..),
defaultRequest)
import qualified Network.Wai as Wai
entireRequestBody :: MonadIO m => Request -> m LB.ByteString
entireRequestBody req = liftIO (requestBody req) >>= strictRequestBody' LB.empty
where strictRequestBody' acc prev
| BS.null prev = return acc
| otherwise = liftIO (requestBody req) >>= strictRequestBody' (acc <> LB.fromStrict prev)
data RequestReader = RequestReader
{ _now :: UTCTime
, _airshipRequest :: AirshipRequest
}
data AirshipRequest = AirshipRequest
{ _request :: Request
, _routePath :: Text
}
data ETag = Strong ByteString
| Weak ByteString
deriving (Eq, Ord)
instance Show ETag where show = unpack . etagToByteString
etagToByteString :: ETag -> ByteString
etagToByteString (Strong bs) = "\"" <> bs <> "\""
etagToByteString (Weak bs) = "W/\"" <> bs <> "\""
data ResponseBody
= ResponseFile FilePath (Maybe Wai.FilePart)
| ResponseBuilder Builder
| ResponseStream Wai.StreamingBody
| Empty
escapedResponse :: Text -> ResponseBody
escapedResponse = ResponseBuilder . fromHtmlEscapedText
data Response = Response { _responseStatus :: Status
, _responseHeaders :: ResponseHeaders
, _responseBody :: ResponseBody
}
data ResponseState = ResponseState { stateHeaders :: ResponseHeaders
, stateBody :: ResponseBody
, _params :: HashMap Text Text
, _dispatchPath :: [Text]
, decisionTrace :: Trace
}
type Trace = [ByteString]
type ErrorResponses m = Monad m => Map HTTP.Status [(MediaType, Webmachine m ResponseBody)]
newtype Webmachine m a =
Webmachine { getWebmachine :: (RST RequestReader ResponseState Response m) a }
deriving (Functor, Applicative, Monad, MonadIO, MonadBase b,
MonadReader RequestReader,
MonadState ResponseState)
instance MonadTrans Webmachine where
lift = Webmachine . RST . helper where
helper m _ s = do
a <- m
return $ (Right a, s)
newtype StMWebmachine m a = StMWebmachine {
unStMWebmachine :: StM (RST RequestReader ResponseState Response m) a
}
instance MonadBaseControl b m => MonadBaseControl b (Webmachine m) where
type StM (Webmachine m) a = StMWebmachine m a
liftBaseWith f = Webmachine
$ liftBaseWith
$ \g' -> f
$ \m -> liftM StMWebmachine
$ g' $ getWebmachine m
restoreM = Webmachine . restoreM . unStMWebmachine
modify'' :: MonadState s m => (s -> s) -> m ()
#if MIN_VERSION_mtl(2,2,0)
modify'' = modify'
#else
modify'' f = state (\s -> let s' = f s in s' `seq` ((), s'))
#endif
request :: Monad m => Webmachine m Request
request = _request . _airshipRequest <$> ask
routePath :: Monad m => Webmachine m Text
routePath = _routePath . _airshipRequest <$> ask
params :: Monad m => Webmachine m (HashMap Text Text)
params = _params <$> get
dispatchPath :: Monad m => Webmachine m [Text]
dispatchPath = _dispatchPath <$> get
requestTime :: Monad m => Webmachine m UTCTime
requestTime = _now <$> ask
getResponseHeaders :: Monad m => Webmachine m ResponseHeaders
getResponseHeaders = stateHeaders <$> get
getResponseBody :: Monad m => Webmachine m ResponseBody
getResponseBody = stateBody <$> get
putResponseBody :: Monad m => ResponseBody -> Webmachine m ()
putResponseBody b = modify'' updateState
where updateState rs = rs {stateBody = b}
putResponseBS :: Monad m => ByteString -> Webmachine m ()
putResponseBS bs = putResponseBody $ ResponseBuilder $ fromByteString bs
halt :: Monad m => Status -> Webmachine m a
halt status = finishWith =<< Response <$> return status <*> getResponseHeaders <*> getResponseBody
finishWith :: Monad m => Response -> Webmachine m a
finishWith = Webmachine . failure
addTrace :: Monad m => ByteString -> Webmachine m ()
addTrace t = modify'' (\s -> s { decisionTrace = t : decisionTrace s })
both :: Either a a -> a
both = either id id
eitherResponse :: Monad m => RequestReader -> ResponseState -> Webmachine m Response -> m (Response, Trace)
eitherResponse requestReader startingState w = do
(e, trace) <- runWebmachine requestReader startingState w
return (both e, trace)
mapWebmachine :: ( m1 (Either Response a1, ResponseState)
-> m2 (Either Response a2, ResponseState))
-> Webmachine m1 a1 -> Webmachine m2 a2
mapWebmachine f = Webmachine . (mapRST f) . getWebmachine
runWebmachine :: Monad m => RequestReader -> ResponseState -> Webmachine m a -> m (Either Response a, Trace)
runWebmachine requestReader startingState w = do
(e, s) <- runRST (getWebmachine w) requestReader startingState
return (e, reverse $ decisionTrace s)