mig-0.1.0.0: src/Mig.hs
{-# Language UndecidableInstances #-}
-- | Main module to write servers
--
-- Server is a function from response (Resp) to request (Req). Request is wrapped into monad.
-- Library supports IO-monad and ReaderT over IO like monads.
--
-- We can build servers from parts with flexible combinators.
-- Let's build hello-world server:
--
-- > main :: IO ()
-- > main = runServer 8080 server
-- >
-- > server :: Server IO
-- > server =
-- > "api" /. "v1" /. "hello" /. Get @Text handleHello
-- >
-- > handleHello :: IO Text
-- > handleHello = pure "Hello World"
--
-- We can iuse monoids to combine servers and newtype-wrappers to read various inputs.
-- See readme of the repo for tutorial and docs.
module Mig
( -- * types
Server (..)
-- * DSL
, Json
-- ** methods
, Get (..)
, Post (..)
, Put (..)
, Delete (..)
, Patch (..)
, Options (..)
-- ** path and query
-- | Build API for routes with queries and captures.
-- Use monoid to combine several routes together.
, (/.)
, Capture (..)
, Query (..)
, Optional (..)
, Body (..)
, RawBody (..)
, Header (..)
, RawFormData (..)
, FormBody (..)
, FormJson (..)
, PathInfo (..)
-- ** response
-- | How to modify response and attach specific info to it
, AddHeaders (..)
, SetStatus (..)
, setStatus
, addHeaders
-- ** Errors
-- | How to report errors
, Error (..)
, handleError
-- * Run
-- | Run server application
, runServer
, ServerConfig (..)
, toApplication
-- ** Render
-- | Render Reader-IO monad servers to IO servers.
, HasServer (..)
, fromReader
-- * Convertes
, ToTextResp (..)
, ToJsonResp (..)
, ToHtmlResp (..)
, FromText (..)
, ToText (..)
-- * utils
, badRequest
, ToServer (..)
, withServerAction
, module X
) where
import Mig.Internal.Types
import Mig.Internal.Types qualified as Resp (Resp (..))
import Data.Bifunctor
import Data.Kind
import Data.String
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import Data.Text.Lazy qualified as TL
import Data.Aeson (ToJSON, FromJSON)
import Data.Aeson qualified as Json
import Data.ByteString (ByteString)
import Data.ByteString.Lazy qualified as BL
import Text.Blaze.Html (Html)
import Text.Blaze.Html (ToMarkup)
import Text.Read (readMaybe)
import Control.Monad.Reader
import Control.Monad.Except
import GHC.TypeLits
import Data.Proxy
import Data.Map.Strict qualified as Map
import Network.HTTP.Types.Status as X
import Network.HTTP.Types.Method
import Network.HTTP.Types.Header (ResponseHeaders)
import Network.Wai.Handler.Warp qualified as Warp
import Control.Exception (throw)
import Data.Typeable
import Data.Aeson.Key qualified as Json
-- | Path constructor (right associative). Example:
--
-- > server :: Server IO
-- > server =
-- > "api" /. "v1" /.
-- > mconcat
-- > [ "foo" /. Get @Json handleFoo
-- > , "bar" /. Post @Json handleBar
-- > ]
-- >
-- > handleFoo, handleBar :: IO Text
(/.) :: ToServer a => Text -> a -> Server (ServerMonad a)
(/.) path act = toWithPath path (toServer act)
infixr 4 /.
-- | Map internal monad of the server
hoistServer :: (forall a . m a -> n a) -> Server m -> Server n
hoistServer f (Server act) = Server (f . act)
-- | Aything convertible from text
class FromText a where
fromText :: Text -> Maybe a
instance FromText ByteString where
fromText = Just . Text.encodeUtf8
instance FromText String where
fromText = Just . Text.unpack
instance FromText Text where
fromText = Just
instance FromText TL.Text where
fromText = Just . TL.fromStrict
instance FromText Word where
fromText = readMaybe . Text.unpack
instance FromText Int where
fromText = readMaybe . Text.unpack
instance FromText Integer where
fromText = readMaybe . Text.unpack
instance FromText Bool where
fromText = readMaybe . Text.unpack
instance FromText Float where
fromText = readMaybe . Text.unpack
newtype QueryName a = QueryName Text
deriving (IsString, Eq, Ord, Show)
toWithQuery :: ByteString -> (Maybe ByteString -> Server m) -> Server m
toWithQuery name act = Server $ \req ->
unServer (act (Map.lookup name req.query)) req
withQuery' :: FromText a => QueryName a -> (Maybe a -> Server m) -> Server m
withQuery' (QueryName name) act = toWithQuery (Text.encodeUtf8 name) $ \mVal ->
let
-- TODO: do not ignore parse failure
mArg = fromText =<< either (const Nothing) Just . Text.decodeUtf8' =<< mVal
in
act mArg
withQuery :: (Applicative m, FromText a) => QueryName a -> (a -> Server m) -> Server m
withQuery (QueryName name) act = toWithQuery (Text.encodeUtf8 name) $ \mVal ->
let
-- TODO: do not ignore parse failure
mArg = fromText =<< either (const Nothing) Just . Text.decodeUtf8' =<< mVal
in
case mArg of
Just arg -> act arg
Nothing -> toConst (pure $ badRequest $ "Failed to parse arg: " <> name)
-- | Class contains types which can be converted to IO-based server to run as with WAI-interface.
--
-- We can run plain IO-servers and ReaderT over IO based servers. Readers can be wrapped in newtypes.
-- In that case we can derive automatically @HasServer@ instance.
class Monad m => HasServer m where
type ServerResult m :: Type
renderServer :: Server m -> ServerResult m
instance HasServer IO where
type ServerResult IO = Server IO
renderServer = id
instance HasServer (ReaderT env IO) where
type ServerResult (ReaderT env IO) = env -> IO (Server IO)
renderServer server initEnv = fromReader initEnv server
-- | Render reader server to IO-based server
fromReader :: env -> Server (ReaderT env IO) -> IO (Server IO)
fromReader env server =
flip runReaderT env $ ReaderT $ \e -> pure $ hoistServer (flip runReaderT e) server
instance (Show a, Typeable a) => HasServer (ReaderT env (ExceptT (Error a) IO)) where
type ServerResult (ReaderT env (ExceptT (Error a) IO)) =
(Error a -> Server IO) -> env -> IO (Server IO)
renderServer server handleErr initEnv = fromReaderExcept handleErr initEnv server
fromReaderExcept ::
(Show a, Typeable a) =>
(Error a -> Server IO) ->
env ->
Server (ReaderT env (ExceptT (Error a) IO)) -> IO (Server IO)
fromReaderExcept handleErr env server =
fmap (handleError handleErr) $
flip runReaderT env $ ReaderT $
\e -> pure $ hoistServer (either throw pure <=< runExceptT . flip runReaderT e) server
-- Prim types
-- | Type tag of Json-response.
data Json
-- server DSL
-- | Class ToServer contains anything convertible to @Server m@. We use it for flexuble composition
-- of servers from functions with arbitrary number of arguments. Arguments can
-- be of specific types: Query, Body, Optional, Capture, Header, etc.
-- We use type-level strings to encode query-names.
-- Example:
--
-- > "api" /. "foo" /.
-- > (\(Query @"argA" argA) (Optional @"argB" argB) (Body jsonRequest) -> Post @Json $ handleFoo argA argB jsonRequest)
-- >
-- > handleFoo :: Int -> Maybe Text -> FooRequest -> IO FooResponse
-- > handleFoo = ...
--
-- Note that we can use any amount of arguments. And type of the input is decoded fron newtype wrapper
-- which is used with argument of the handler function.
--
-- Also we can return pure errors with Either. Anything which can be returned from function
-- can be wrapped to @Either (Error err)@.
--
-- For example in previous case we can use function which returns errors as values:
--
-- > type ServerError = Error Text
-- >
-- > handleFoo :: Int -> Maybe Text -> FooRequest -> IO (Either ServerError FooResponse)
-- > handleFoo = ...
--
-- the result of error response is automatically matched with normal response of the server
-- and standard Error type lets us pass status to response and some details.
class Monad (ServerMonad a) => ToServer a where
type ServerMonad a :: (Type -> Type)
toServer :: a -> Server (ServerMonad a)
instance Monad m => ToServer (Server m) where
type ServerMonad (Server m) = m
toServer = id
-- Status
-- | Set status to response. It can be ised inside any ToXxxResp value. Example:
--
-- > "api" /. handleFoo
-- >
-- > handleFoo :: Get Text IO (SetStatus Text)
-- > handleFoo = Get $ pure $ SetStatus status500 "Bad request"
data SetStatus a = SetStatus
{ status :: Status
, content :: a
}
-- | Sets status for response of the server
setStatus :: Monad m => Status -> Server m -> Server m
setStatus st = mapResp $ \resp -> resp { Resp.status = st }
-- Headers
-- | Attach headers to response. It can be used inside any ToXxxResp value.
-- Example:
--
-- > "api" /. handleFoo
-- >
-- > handleFoo :: Get Text IO (AddHeaders Text)
-- > handleFoo = Get $ pure $ AddHeaders headers "Hello foo"
data AddHeaders a = AddHeaders
{ headers :: ResponseHeaders
, content :: a
}
-- | Adds headers for response of the server
addHeaders :: Monad m => ResponseHeaders -> Server m -> Server m
addHeaders headers = mapResp $ \resp -> resp { Resp.headers = resp.headers <> headers }
mapResp :: Monad m => (Resp -> Resp) -> Server m -> Server m
mapResp f (Server act) = Server $ \req ->
fmap (fmap f) $ act req
-- text response
-- | Values convertible to Text (lazy)
class ToTextResp a where
toTextResp :: a -> Resp
instance ToTextResp Text where
toTextResp = text
instance ToTextResp TL.Text where
toTextResp = text
instance ToTextResp Int where
toTextResp = text
instance ToTextResp a => ToTextResp (AddHeaders a) where
toTextResp (AddHeaders headers content) =
resp { Resp.headers = resp.headers <> headers }
where
resp = toTextResp content
instance ToTextResp a => ToTextResp (SetStatus a) where
toTextResp (SetStatus st content) =
setRespStatus st (toTextResp content)
instance (ToText err, ToTextResp a) => ToTextResp (Either (Error err) a) where
toTextResp = either fromError toTextResp
where
fromError err = setRespStatus err.status (text err.body)
-- json response
-- | Values convertible to Json
class ToJsonResp a where
toJsonResp :: a -> Resp
instance {-# OVERLAPPABLE #-} ToJSON a => ToJsonResp a where
toJsonResp = json
instance ToJsonResp a => ToJsonResp (AddHeaders a) where
toJsonResp (AddHeaders headers content) =
resp { Resp.headers = resp.headers <> headers }
where
resp = toJsonResp content
instance ToJsonResp a => ToJsonResp (SetStatus a) where
toJsonResp (SetStatus st content) =
setRespStatus st (toJsonResp content)
instance (ToJSON err, ToJsonResp a) => ToJsonResp (Either (Error err) a) where
toJsonResp = either fromError toJsonResp
where
fromError err = setRespStatus err.status (json err.body)
-- html response
-- | Values convertible to Html
class ToHtmlResp a where
toHtmlResp :: a -> Resp
instance ToMarkup a => ToHtmlResp a where
toHtmlResp = html
instance ToHtmlResp a => ToHtmlResp (AddHeaders a) where
toHtmlResp (AddHeaders headers content) =
resp { Resp.headers = resp.headers <> headers }
where
resp = toHtmlResp content
instance ToHtmlResp a => ToHtmlResp (SetStatus a) where
toHtmlResp (SetStatus st content) =
setRespStatus st (toHtmlResp content)
instance (ToJSON err, ToHtmlResp a) => ToHtmlResp (Either (Error err) a) where
toHtmlResp = either fromError toHtmlResp
where
fromError err = setRespStatus err.status (json err.body)
-- Get
-- | Get method. Note that we can not use body input with Get-method, use Post for that.
-- So with Get we can use only URI inputs (Query, Optional, Capture)
newtype Get ty m a = Get (m a)
instance (Monad m, ToTextResp a) => ToServer (Get Text m a) where
type ServerMonad (Get Text m a) = m
toServer (Get act) = toMethod methodGet (toTextResp <$> act)
instance (Monad m, ToJSON a) => ToServer (Get Json m a) where
type ServerMonad (Get Json m a) = m
toServer (Get act) = toMethod methodGet (json <$> act)
instance (Monad m, ToHtmlResp a) => ToServer (Get Html m a) where
type ServerMonad (Get Html m a) = m
toServer (Get act) = toMethod methodGet (toHtmlResp <$> act)
instance (Monad m) => ToServer (Get BL.ByteString m BL.ByteString) where
type ServerMonad (Get BL.ByteString m BL.ByteString) = m
toServer (Get act) = toMethod methodGet (raw <$> act)
instance (Monad m) => ToServer (Get ByteString m ByteString) where
type ServerMonad (Get ByteString m ByteString) = m
toServer (Get act) = toMethod methodGet (raw . BL.fromStrict <$> act)
-- Post
-- | Post method
newtype Post ty m a = Post (m a)
instance (Monad m, ToTextResp a) => ToServer (Post Text m a) where
type ServerMonad (Post Text m a) = m
toServer (Post act) = toMethod methodPost $ toTextResp <$> act
instance (Monad m, ToJSON a) => ToServer (Post Json m a) where
type ServerMonad (Post Json m a) = m
toServer (Post act) = toMethod methodPost $ json <$> act
instance (Monad m, ToHtmlResp a) => ToServer (Post Html m a) where
type ServerMonad (Post Html m a) = m
toServer (Post act) = toMethod methodPost (toHtmlResp <$> act)
-- Put
-- | Put method
newtype Put ty m a = Put (m a)
instance (Monad m, ToTextResp a) => ToServer (Put Text m a) where
type ServerMonad (Put Text m a) = m
toServer (Put act) = toMethod methodPut $ toTextResp <$> act
instance (Monad m, ToJSON a) => ToServer (Put Json m a) where
type ServerMonad (Put Json m a) = m
toServer (Put act) = toMethod methodPut $ json <$> act
instance (Monad m, ToHtmlResp a) => ToServer (Put Html m a) where
type ServerMonad (Put Html m a) = m
toServer (Put act) = toMethod methodPut (toHtmlResp <$> act)
-- Delete
-- | Delete method
newtype Delete ty m a = Delete (m a)
instance (Monad m, ToTextResp a) => ToServer (Delete Text m a) where
type ServerMonad (Delete Text m a) = m
toServer (Delete act) = toMethod methodDelete $ toTextResp <$> act
instance (Monad m, ToJSON a) => ToServer (Delete Json m a) where
type ServerMonad (Delete Json m a) = m
toServer (Delete act) = toMethod methodDelete $ json <$> act
instance (Monad m, ToHtmlResp a) => ToServer (Delete Html m a) where
type ServerMonad (Delete Html m a) = m
toServer (Delete act) = toMethod methodDelete (toHtmlResp <$> act)
-- Patch
-- | Patch method
newtype Patch ty m a = Patch (m a)
instance (Monad m, ToTextResp a) => ToServer (Patch Text m a) where
type ServerMonad (Patch Text m a) = m
toServer (Patch act) = toMethod methodPatch $ toTextResp <$> act
instance (Monad m, ToJSON a) => ToServer (Patch Json m a) where
type ServerMonad (Patch Json m a) = m
toServer (Patch act) = toMethod methodPatch $ json <$> act
instance (Monad m, ToHtmlResp a) => ToServer (Patch Html m a) where
type ServerMonad (Patch Html m a) = m
toServer (Patch act) = toMethod methodPatch (toHtmlResp <$> act)
-- Options
-- | Options method
newtype Options ty m a = Options (m a)
instance (Monad m, ToTextResp a) => ToServer (Options Text m a) where
type ServerMonad (Options Text m a) = m
toServer (Options act) = toMethod methodOptions $ toTextResp <$> act
instance (Monad m, ToJSON a) => ToServer (Options Json m a) where
type ServerMonad (Options Json m a) = m
toServer (Options act) = toMethod methodOptions $ json <$> act
instance (Monad m, ToHtmlResp a) => ToServer (Options Html m a) where
type ServerMonad (Options Html m a) = m
toServer (Options act) = toMethod methodOptions (toHtmlResp <$> act)
-- Query
-- | Mandatary query parameter. Name is encoded as type-level string. Example:
--
-- > "api" /. handleFoo
-- >
-- > handleFoo :: Query "name" Int -> Server IO
-- > handleFoo (Query arg) = ...
newtype Query (sym :: Symbol) a = Query a
instance (FromText a, ToServer b, KnownSymbol sym) => ToServer (Query sym a -> b) where
type ServerMonad (Query sym a -> b) = ServerMonad b
toServer act = withQuery (QueryName (Text.pack $ symbolVal (Proxy @sym))) (toServer . act . Query)
-- Optional query
-- | Optional query parameter. Name is encoded as type-level string. Example:
--
-- > "api" /. handleFoo
-- >
-- > handleFoo :: Optional "name" -> Server IO
-- > handleFoo (Optional maybeArg) = ...
newtype Optional (sym :: Symbol) a = Optional (Maybe a)
instance (FromText a, ToServer b, KnownSymbol sym) => ToServer (Optional sym a -> b) where
type ServerMonad (Optional sym a -> b) = ServerMonad b
toServer act = withQuery' (QueryName (fromString $ symbolVal (Proxy @sym))) (toServer . act . Optional)
-- Capture
-- Captures part of the path. Example
--
-- "api" /. "foo" /. (\(Capture n) -> handleFoo n)
--
-- It will parse the paths: "api/foo/358" and pass 358 to @handleFoo@.
newtype Capture a = Capture a
instance (FromText a, ToServer b) => ToServer (Capture a -> b) where
type ServerMonad (Capture a -> b) = ServerMonad b
toServer act = toWithCapture $ \txt ->
case fromText txt of
Just val -> toServer $ act $ Capture val
Nothing -> toConst $ pure $ badRequest "Failed to parse capture"
-- Read Body input
-- | Reads Json body (lazy). We can limit the body size with server config. Example:
--
-- > "api" /. "search" /. (\(Body request) -> handleSearch request)
newtype Body a = Body a
instance (MonadIO (ServerMonad b), FromJSON a, ToServer b) => ToServer (Body a -> b) where
type ServerMonad (Body a -> b) = ServerMonad b
toServer act = toWithBody $ \val ->
case Json.eitherDecode val of
Right v -> toServer $ act $ Body v
Left err -> toConst $ pure $ badRequest $ "Failed to parse JSON body: " <> Text.pack err
-- | Reads raw body as lazy bytestring. We can limit the body size with server config. Example:
--
-- > "api" /. "upload" /. (\(RawBody content) -> handleUpload content)
newtype RawBody = RawBody BL.ByteString
instance (MonadIO (ServerMonad b), ToServer b) => ToServer (RawBody -> b) where
type ServerMonad (RawBody -> b) = ServerMonad b
toServer act = toWithBody $ toServer . act . RawBody
-- | Parse raw form body. It includes named form arguments and file info.
-- Note that we can not use FormBody and JSON-body at the same time.
-- They occupy the same field in the HTTP-request.
newtype RawFormData = RawFormData FormBody
instance (ToServer b, MonadIO (ServerMonad b)) => ToServer (RawFormData -> b) where
type ServerMonad (RawFormData -> b) = ServerMonad b
toServer act = toWithFormData $ toServer . act . RawFormData
-- | It reads form as plain JSON-object where name of the form's field becomes
-- a field of JSON-object and every value is Text.
--
-- For example if submit a form with fields: name, password, date.
-- We can read it in the data type:
--
-- > data User = User
-- > { name :: Text
-- > , passord :: Text
-- > , date :: Text
-- > }
--
-- Note that we can not use FormBody and JSON-body at the same time.
-- They occupy the same field in the HTTP-request.
newtype FormJson a = FormJson a
instance (ToServer b, MonadIO (ServerMonad b), FromJSON a) => ToServer (FormJson a -> b) where
type ServerMonad (FormJson a -> b) = ServerMonad b
toServer act = toWithFormData $ \formBody -> do
case formDataToJson formBody.params of
Right v -> toServer $ act $ FormJson v
Left err -> toConst $
pure $ badRequest $ "Failed to parse form data as JSON body: " <> err
formDataToJson :: FromJSON a => [(ByteString, ByteString)] -> Either Text a
formDataToJson rawPairs = do
jsonVal <- Json.object <$> mapM toPair rawPairs
case Json.fromJSON jsonVal of
Json.Success result -> Right result
Json.Error err -> Left (Text.pack $ show err)
where
toPair (key, val) =
first (Text.pack . show) $
(\k v -> (Json.fromText k, Json.String v)) <$> Text.decodeUtf8' key <*> Text.decodeUtf8' val
-- Request Headers
-- | Reads input header. Example:
--
-- > "api" /. (\(Header @"Trace-Id" traceId) -> Post @Json (handleFoo traceId))
-- >
-- > handleFoo :: Maybe ByteString -> IO FooResponse
newtype Header (sym :: Symbol) = Header (Maybe ByteString)
instance (ToServer b, KnownSymbol sym) => ToServer (Header sym -> b) where
type ServerMonad (Header sym -> b) = ServerMonad b
toServer act = toWithHeader (fromString $ symbolVal (Proxy @sym)) (toServer . act . Header)
-- | Reads current path info
newtype PathInfo = PathInfo [Text]
instance (ToServer b) => ToServer (PathInfo -> b) where
type ServerMonad (PathInfo -> b) = ServerMonad b
toServer act = toWithPathInfo (toServer . act . PathInfo)
-- | Appends action to the server
withServerAction :: Monad m => Server m -> m () -> Server m
withServerAction srv act = Server $ \req -> do
act
unServer srv req
-------------------------------------------------------------------------------------
-- WAI
-- | Run server on port
runServer :: Int -> Server IO -> IO ()
runServer port server =
Warp.run port (toApplication config server)
where
config = ServerConfig { maxBodySize = Nothing }