packages feed

minion-0.1.0.0: src/Web/Minion/Router/Internal.hs

{-# LANGUAGE PartialTypeSignatures #-}

module Web.Minion.Router.Internal where

import Control.Monad ((>=>))
import Control.Monad.IO.Class qualified as IO
import Data.ByteString (ByteString)
import Data.String (IsString (..))
import Data.Text (Text)
import GHC.Exts (IsList (..))
import Network.HTTP.Types qualified as Http
import Network.Wai qualified as Http
import Network.Wai qualified as Wai

import Control.Exception qualified as IOExc
import Control.Monad.Catch qualified as Exc
import Data.ByteString.Lazy qualified as Bytes.Lazy
import Data.Kind (Type)
import Data.Void (Void)
import Web.Minion.Args.Internal (
  Arg,
  FunArgs (apply, type (~>)),
  HList,
  HandleArgs,
  IsLenient,
  IsRequired,
  RHList ((:#!)),
  RHListToHList (revHListToList),
  Reverse (reverseHList),
  RunDelayed (DelayedArgs, runDelayed),
  WithHeader (..),
  WithPiece (..),
  WithPieces (..),
  WithQueryParam (..),
  WithReq (..),
  type (:+),
 )
import Web.Minion.Error (
  ErrorBuilder,
  ErrorBuilders (..),
  NoMatch (..),
  ServerError,
 )
import Web.Minion.Introspect qualified as I
import Web.Minion.Request (IsRequest)
import Web.Minion.Response (CanRespond (..), ToResponse (..))

-- | If you don't care about introspection
type Router = Router' Void

type MiddlewareM m = ApplicationM m -> ApplicationM m

type MakeError = Http.Status -> Bytes.Lazy.ByteString -> ServerError

type ValueCombinator i v ts m = Router' i (ts :+ v) m -> Router' i ts m
type Combinator i ts m = Router' i ts m -> Router' i ts m

data Router' i (ts :: Type) m where
  Piece ::
    -- | .
    Text ->
    Router' i ts m ->
    Router' i ts m
  QueryParam ::
    forall a presence parsing m ts i.
    (I.Introspection i I.QueryParam a, IsRequired presence, IsLenient parsing) =>
    -- | Query param name
    ByteString ->
    -- | Parse query param
    -- Outer Maybe -- is there key
    -- Inner Maybe -- is there value
    (MakeError -> Maybe (Maybe ByteString) -> m (Arg presence parsing a)) ->
    Router' i (ts :+ WithQueryParam presence parsing m a) m ->
    Router' i ts m
  Captures ::
    forall a ts m i.
    (I.Introspection i I.Captures a) =>
    -- | Parse pieces
    (MakeError -> [Text] -> m [a]) ->
    -- | Placeholder
    Text ->
    Router' i (ts :+ WithPieces a) m ->
    Router' i ts m
  Capture ::
    forall a ts m i.
    (I.Introspection i I.Capture a) =>
    -- | Parse piece
    (MakeError -> Text -> m a) ->
    -- | Placeholder
    Text ->
    Router' i (ts :+ WithPiece a) m ->
    Router' i ts m
  Middleware ::
    MiddlewareM m ->
    Router' i ts m ->
    Router' i ts m
  Header ::
    forall a presence parsing m ts i.
    (I.Introspection i I.Header a, IsRequired presence, IsLenient parsing) =>
    Http.HeaderName ->
    -- | Parse header
    (MakeError -> [ByteString] -> m (Arg presence parsing a)) ->
    Router' i (ts :+ WithHeader presence parsing m a) m ->
    Router' i ts m
  Request ::
    forall r m i ts.
    (I.Introspection i I.Request r, IsRequest r) =>
    -- | .
    (ErrorBuilder -> Wai.Request -> m r) ->
    Router' i (ts :+ WithReq m r) m ->
    Router' i ts m
  Alt ::
    -- | Sub routes
    [Router' i ts m] ->
    Router' i ts m
  -- -- | Additional constraints provider with `request` and `response` can be useful for introspection
  Handle ::
    forall o m ts i st.
    ( HandleArgs ts st m
    , ToResponse m o
    , CanRespond o
    , I.Introspection i I.Response o
    ) =>
    -- | Handled HTTP method
    Http.Method ->
    (HList (DelayedArgs st) -> m o) ->
    Router' i ts m
  Description ::
    (I.Introspection i I.Description desc) =>
    -- | .
    desc ->
    Router' i ts m ->
    Router' i ts m
  MapArgs ::
    forall m ts ts' i.
    (RHList ts -> RHList ts') ->
    Router' i ts' m ->
    Router' i ts m
  HideIntrospection ::
    forall i' i ts m.
    Router' i ts m ->
    Router' i' ts m

{-# INLINE route #-}
route ::
  forall m ts i.
  (IO.MonadIO m, Exc.MonadCatch m) =>
  ErrorBuilders ->
  RoutingState ->
  RHList ts ->
  Router' i ts m ->
  ApplicationM m
route builders state args (Alt routes) = \req resp -> goThrough $ map (\r -> route builders state args r req resp) routes
route builders state args (Middleware mw r) = mw (route builders state args r)
route builders state args (MapArgs f r) = route builders state (f args) r
route builders state args (Description _ r) = route builders state args r
route builders state args (HideIntrospection r) = route builders state args r
route _ RoutingState{..} args (Handle @o method f) = routeHandle path args method f
route builders@ErrorBuilders{..} state args (Request @f get r) = \req resp -> do
  route builders state (WithReq (get bodyErrorBuilder req) :#! args) r req resp
route builders@ErrorBuilders{..} state args (Header @a @presence @parsing headerName get r) = \req ->
  let header = lookupHeader req headerName
      withHeader = WithHeader (get (headerErrorBuilder req) header) :#! args
   in route builders state withHeader r req
route builders@ErrorBuilders{..} state args (QueryParam @a @presence @parsing queryParamName parse r) = \req ->
  let mbQueryParamVal = lookup queryParamName $ Http.queryString req
      withQueryParam = WithQueryParam (parse (queryParamsErrorBuilder req) mbQueryParamVal) :#! args
   in route builders state withQueryParam r req
route builders RoutingState{..} args (Piece txt r) = case path of
  (t : ts) | txt == t -> route builders RoutingState{path = ts, ..} args r
  _ -> \_ _ -> throwMIO NoMatch
route builders@ErrorBuilders{..} RoutingState{..} args (Captures parse _ r) = \req resp -> do
  parsed <- parse (captureErrorBuilder req) path
  route builders RoutingState{path = [], ..} (WithPieces parsed :#! args) r req resp
route builders@ErrorBuilders{..} RoutingState{..} args (Capture parse _ r) = \req resp -> case path of
  (t : ts) -> do
    v <- parse (captureErrorBuilder req) t
    route builders RoutingState{path = ts, ..} (WithPiece v :#! args) r req resp
  _ -> throwMIO NoMatch

{-# INLINE routeHandle #-}
routeHandle ::
  forall m o ts st.
  (IO.MonadIO m, ToResponse m o, CanRespond o, HandleArgs ts st m) =>
  [Text] ->
  RHList ts ->
  Http.Method ->
  (HList (DelayedArgs st) -> m o) ->
  ApplicationM m
routeHandle path args method f req resp = do
  checkHandler req path method
  let acceptHeader = lookupHeader req Http.hAccept
  if canRespond @o acceptHeader
    then do
      args' <- runDelayed (reverseHList (revHListToList args))
      f args' >>= (toResponse @m @o acceptHeader >=> IO.liftIO . resp)
    else IO.liftIO $ resp $ Wai.responseBuilder Http.status406 [] mempty

{-# INLINE goThrough #-}
goThrough :: (IO.MonadIO m, Exc.MonadCatch m) => [m b] -> m b
goThrough (a : as) =
  Exc.try @_ @NoMatch a >>= \case
    Left NoMatch -> goThrough as
    Right x -> pure x
goThrough [] = throwMIO NoMatch

{-# INLINE throwMIO #-}
throwMIO :: (Exc.Exception e, IO.MonadIO m) => e -> m a
throwMIO = IO.liftIO . IOExc.throwIO

{-# INLINE checkHandler #-}
checkHandler :: (IO.MonadIO f) => Wai.Request -> [Text] -> Http.Method -> f ()
checkHandler req path method
  | method == Http.requestMethod req
  , null path || path == [mempty] =
      pure ()
  | otherwise = throwMIO NoMatch

{-# INLINE lookupHeader #-}
lookupHeader :: Wai.Request -> Http.HeaderName -> [ByteString]
lookupHeader req hn = map snd . filter ((hn ==) . fst) $ Http.requestHeaders req

instance IsList (Router' i ts r) where
  type Item (Router' i ts r) = Router' i ts r
  fromList = Alt
  toList a = [a]

instance Semigroup (Router' i ts r) where
  a <> b = [a, b]

instance Monoid (Router' i ts r) where
  mempty = []

instance IsString (Combinator i ts m) where
  {-# INLINE fromString #-}
  fromString = smartPiece

{-# INLINE smartPiece #-}
smartPiece :: String -> Combinator i ts m
smartPiece (break (== '/') -> (a, as)) cont =
  if null as
    then Piece (fromString a) cont
    else Piece (fromString a) (smartPiece (drop 1 as) cont)

newtype RoutingState = RoutingState {path :: [Text]}

-- | 'Wai.Application' lifted to `m`
type ApplicationM m =
  Wai.Request ->
  ( Wai.Response ->
    IO Wai.ResponseReceived
  ) ->
  m Wai.ResponseReceived

{-# INLINE makeHandle #-}
makeHandle ::
  forall f o m ts i st.
  ( HandleArgs ts st m
  , ToResponse m (f o)
  , CanRespond (f o)
  , I.Introspection i I.Response (f o)
  ) =>
  Http.Method ->
  (o -> f o) ->
  (DelayedArgs st ~> m o) ->
  Router' i ts m
makeHandle method packResponse f =
  Handle @(f o) method (fmap packResponse . apply f)