packages feed

minion-0.1.0.0: src/Web/Minion.hs

{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE PartialTypeSignatures #-}

module Web.Minion (
  -- * Minion
  Router',
  Router,
  MakeError,

  -- * Combinators
  ValueCombinator,
  Combinator,
  alt,
  (/>),
  (.>),
  (!>),
  hideIntrospection,
  description,

  -- ** Header
  module Web.Minion.Request.Header,

  -- ** Query params
  module Web.Minion.Request.Query,

  -- ** URL
  module Web.Minion.Request.Url,

  -- ** Request
  ReqBody (..),
  reqBody,
  reqPlainText,
  reqFormUrlEncoded,
  reqJson,
  LazyBytes (..),
  lazyBytesBody,
  Chunks (..),
  chunksBody,

  -- ** Response
  NoBody (..),
  ToResponse (..),
  CanRespond (..),

  -- ** Handler
  handle,
  handleJson,
  handlePlainText,
  RespBody (..),
  handleBody,
  module Web.Minion.Request.Method,

  -- ** Middleware
  MiddlewareM,
  middleware,

  -- ** Server
  ApplicationM,
  MinionSettings (..),
  serve,
  serveWithSettings,
  defaultMinionSettings,
  defaultErrorBuilders,

  -- ** Exceptions
  NoMatch (..),
  SomethingWentWrong (..),
  ServerError (..),

  -- ** Args
  module Web.Minion.Args,

  -- ** Auth
  module Web.Minion.Auth,

  -- ** Reexports
  Void,
  Exc.MonadCatch (..),
  Exc.MonadThrow (..),
) where

import Control.Monad ((>=>))
import Control.Monad.Catch qualified as Exc
import Control.Monad.IO.Class qualified as IO
import Data.Binary.Builder qualified as Bytes.Builder
import Data.Text qualified as Text
import Data.Void (Void)
import Network.HTTP.Types qualified as Http
import Network.Wai qualified as Http
import Network.Wai qualified as Wai
import Web.Minion.Args
import Web.Minion.Args.Internal
import Web.Minion.Auth
import Web.Minion.Error (
  ErrorBuilders (..),
  NoMatch (..),
  ServerError (..),
  SomethingWentWrong (..),
 )

import Web.Minion.Introspect qualified as I
import Web.Minion.Json (handleJson, reqJson)
import Web.Minion.Raw
import Web.Minion.Request.Body (ReqBody (..), reqBody)
import Web.Minion.Request.Body.FormUrlEncoded
import Web.Minion.Request.Body.PlainText
import Web.Minion.Request.Body.Raw
import Web.Minion.Request.Header
import Web.Minion.Request.Method
import Web.Minion.Request.Query
import Web.Minion.Request.Url
import Web.Minion.Response
import Web.Minion.Response.Body (RespBody (RespBody), handleBody)
import Web.Minion.Response.Body.PlainText (handlePlainText)
import Web.Minion.Router.Internal

-- | Use it if you don't care about value captured by previous combinator
{-# INLINE (!>) #-}
(!>) ::
  (Router' i (ts :+ x) r -> Router' i ts r) ->
  -- | .
  Router' i (ts :+ Hide x) r ->
  Router' i ts r
a !> b = a .> MapArgs (\case (x :#! as) -> Hide x :#! as) .> b

-- | Use it after 'Combinator'
{-# INLINE (/>) #-}
(/>) ::
  (Router' i ts r -> Router' i ts r) ->
  -- | .
  Router' i ts r ->
  Router' i ts r
(/>) = id

-- | Use it after 'ValueCombinator' and 'MapArgs'
{-# INLINE (.>) #-}
(.>) ::
  (Router' i ts' r -> Router' i ts r) ->
  -- | .
  Router' i ts' r ->
  Router' i ts r
(.>) = id

infixr 0 .>

infixr 0 />

infixr 0 !>

{- | Could be omitted with `OverloadedLists`

@
{\-# LANGUAGE OverloadedLists #-\}
"foo" '/>'
  [ "bar" '/>' ...
  , "baz" '/>' ...
  ]
@

@
{\-# LANGUAGE NoOverloadedLists #-\}
"foo" '/>' 'alt'
  [ "bar" '/>' ...
  , "baz" '/>' ...
  ]
@
-}
{-# INLINE alt #-}
alt :: [Router' i ts r] -> Router' i ts r
alt = Alt

{- | Handles request with specified HTTP method

@
... '/>' 'handle' \@MyResponse GET someEndpoint
@
-}
{-# INLINE handle #-}
handle ::
  forall o m ts i st.
  ( HandleArgs ts st m
  , ToResponse m o
  , CanRespond o
  , I.Introspection i I.Response o
  ) =>
  -- | .
  Http.Method ->
  (DelayedArgs st ~> m o) ->
  Router' i ts m
handle method f = Handle @o method (apply f)

-- | Add description for route
description :: (I.Introspection i I.Description a) => a -> Combinator i ts m
description = Description

hideIntrospection :: Router' i ts m -> Router' i' ts m
hideIntrospection = HideIntrospection

{- | Injects middleware

@
... '/>' 'middleware' Wai.realIp '/>' ...
@
-}
middleware :: MiddlewareM m -> Combinator i ts m
middleware = Middleware

data MinionSettings m = MinionSettings
  { notFound :: m Wai.Response
  , httpError :: ServerError -> m Wai.Response
  , errorBuilders :: ErrorBuilders
  }

{-# INLINE serve #-}
serve :: (IO.MonadIO m, Exc.MonadCatch m) => Router' i Void m -> ApplicationM m
serve = serveWithSettings defaultMinionSettings

defaultMinionSettings :: (IO.MonadIO m, Exc.MonadCatch m) => MinionSettings m
defaultMinionSettings =
  MinionSettings
    { notFound = pure (Wai.responseBuilder Http.status404 [] mempty)
    , httpError = \ServerError{..} -> pure $ Wai.responseBuilder code headers (Bytes.Builder.fromLazyByteString body)
    , errorBuilders = defaultErrorBuilders
    }

defaultErrorBuilders :: ErrorBuilders
defaultErrorBuilders =
  ErrorBuilders
    { headerErrorBuilder = defaultBuilder
    , queryParamsErrorBuilder = defaultBuilder
    , captureErrorBuilder = defaultBuilder
    , bodyErrorBuilder = defaultBuilder
    }
 where
  defaultBuilder _ status = ServerError status []

-- | The same as 'serve' but allows to configure exceptions handlers
{-# INLINE serveWithSettings #-}
serveWithSettings :: (IO.MonadIO m, Exc.MonadCatch m) => MinionSettings m -> Router' i Void m -> ApplicationM m
serveWithSettings MinionSettings{..} router req resp =
  Exc.catches @[]
    (route errorBuilders (RoutingState (filter (not . Text.null) $ Http.pathInfo req)) RHNil router req resp)
    [ Exc.Handler \NoMatch -> notFound >>= IO.liftIO . resp
    , Exc.Handler $ httpError >=> IO.liftIO . resp
    ]