mig-wai-0.1.0.0: src/Mig/Server/Wai.hs
-- | Converts mig server to WAI-application.
module Mig.Server.Wai (
ServerConfig (..),
FindRouteType (..),
Kilobytes,
toApplication,
) where
import Control.Monad.Catch
import Data.ByteString qualified as B
import Data.ByteString.Lazy qualified as BL
import Data.Default
import Data.Foldable
import Data.IORef
import Data.Map.Strict qualified as Map
import Data.Maybe
import Data.Sequence (Seq (..), (|>))
import Data.Sequence qualified as Seq
import Data.Text (Text)
import Network.Wai qualified as Wai
import Mig.Core
import Mig.Core.Server.Cache
-- | Size of the input body
type Kilobytes = Int
-- | Server config
data ServerConfig = ServerConfig
{ maxBodySize :: Maybe Kilobytes
-- ^ limit the request body size. By default it is unlimited.
, cache :: Maybe CacheConfig
-- ^ LRU cache if needed (default is no cache)
, findRoute :: FindRouteType
-- ^ API normal form and find route strategy (default is plain api finder)
}
instance Default ServerConfig where
def = ServerConfig Nothing Nothing PlainFinder
-- | Algorithm to find route handlers by path
data FindRouteType
= -- | converts api to tree-like structure (prefer it for servers with many routes)
TreeFinder
| -- | no optimization (prefer it for small servers)
PlainFinder
-- | Converts mig server to WAI-application.
-- Note that only IO-based servers are supported. To use custom monad
-- we can use @hoistServer@ function which renders monad to IO based or
-- the class @HasServer@ which defines such transformatio for several useful cases.
toApplication :: ServerConfig -> Server IO -> Wai.Application
toApplication config = case config.cache of
Just cacheConfig ->
case config.findRoute of
TreeFinder -> toApplicationWithCache cacheConfig config treeApiStrategy
PlainFinder -> toApplicationWithCache cacheConfig config plainApiStrategy
Nothing ->
case config.findRoute of
TreeFinder -> toApplicationNoCache config treeApiStrategy
PlainFinder -> toApplicationNoCache config plainApiStrategy
-- | Convert server to WAI-application
toApplicationNoCache :: ServerConfig -> FindRoute nf IO -> Server IO -> Wai.Application
toApplicationNoCache config findRoute server req procResponse = do
mResp <- handleServerError onErr (fromServer findRoute server) =<< fromRequest config.maxBodySize req
procResponse $ toWaiResponse $ fromMaybe noResult mResp
where
noResult = badRequest @Text ("Server produces nothing" :: Text)
onErr :: SomeException -> ServerFun IO
onErr err = const $ pure $ Just $ badRequest @Text $ "Error: Exception has happened: " <> toText (show err)
-- | Convert server to WAI-application
toApplicationWithCache :: CacheConfig -> ServerConfig -> FindRoute nf IO -> Server IO -> Wai.Application
toApplicationWithCache cacheConfig config findRoute server req procResponse = do
cache <- newRouteCache cacheConfig
mResp <- handleServerError onErr (fromServerWithCache findRoute cache server) =<< fromRequest config.maxBodySize req
procResponse $ toWaiResponse $ fromMaybe noResult mResp
where
noResult = badRequest @Text ("Server produces nothing" :: Text)
onErr :: SomeException -> ServerFun IO
onErr err = const $ pure $ Just $ badRequest @Text $ "Error: Exception has happened: " <> toText (show err)
-- | Convert response to low-level WAI-response
toWaiResponse :: Response -> Wai.Response
toWaiResponse resp =
case resp.body of
FileResp file -> Wai.responseFile resp.status resp.headers file Nothing
RawResp _ str -> lbs str
StreamResp -> undefined -- TODO
where
lbs = Wai.responseLBS resp.status resp.headers
{-| Read request from low-level WAI-request
First argument limits the size of input body. The body is read in chunks.
-}
fromRequest :: Maybe Kilobytes -> Wai.Request -> IO Request
fromRequest maxSize req = do
bodyCache <- newBodyCache
pure $
Request
{ path = Wai.pathInfo req
, query = Map.fromList (Wai.queryString req)
, headers = Map.fromList $ Wai.requestHeaders req
, method = Wai.requestMethod req
, readBody = readBodyCache getRequestBody bodyCache
, capture = mempty
, isSecure = Wai.isSecure req
}
where
getRequestBody =
fmap (fmap BL.fromChunks) $ readRequestBody (Wai.getRequestBodyChunk req) maxSize
newtype BodyCache a = BodyCache (IORef (Maybe a))
newBodyCache :: IO (BodyCache a)
newBodyCache = BodyCache <$> newIORef Nothing
readBodyCache :: IO a -> BodyCache a -> IO a
readBodyCache getter (BodyCache ref) = do
mVal <- readIORef ref
case mVal of
Just val -> pure val
Nothing -> do
val <- getter
writeIORef ref (Just val)
pure val
-- | Read request body in chunks. Note that this function can be used only once
readRequestBody :: IO B.ByteString -> Maybe Kilobytes -> IO (Either Text [B.ByteString])
readRequestBody readChunk maxSize = loop 0 Seq.empty
where
loop :: Kilobytes -> Seq B.ByteString -> IO (Either Text [B.ByteString])
loop !currentSize !result
| isBigger currentSize = pure outOfSize
| otherwise = do
chunk <- readChunk
if B.null chunk
then pure $ Right (toList result)
else loop (currentSize + B.length chunk) (result |> chunk)
outOfSize :: Either Text a
outOfSize = Left "Request is too big Jim!"
isBigger = case maxSize of
Just size -> \current -> current > size
Nothing -> const False