{-| Plugins are useful to apply certain action to all routes in the server. For example we can add generic logger or authorization bazed on common query parameter or field of the body request that contains token of the session. The downside is that we work on low level of Requesnce/Response as we have rendered all routes to ServerFun. But thw good part of it is that we can add generic action to every route. Let's consider a simple example of adding logger to lall routes: > logRoutes :: Server IO -> Server IO > logRoutes = applyPlugin $ \(PathInfo path) -> prependServerAction $ > when (path /= ["favicon.ico"] && headMay path /= Just "static") $ do > logRoute site (Text.intercalate "/" path) > > -- | Logs the route info > logRoute :: Site -> Text -> IO () > logRoute site route = do > time <- getCurrentTime > site.logInfo $ route <> " page visited at: " <> Text.pack (show time) Here we use instance of ToPlugin for `PathInfo` to read full path for any route and we use this information in the logger. We have various instances for everything that we can query from the request and we can use this information to transform the server functions inside the routes. The instances work in the same manner as route handlers we can use as many arguments as we wish and we use typed wrappers to query specific part of the request. Thus we gain type-safety and get convenient interface to request the various parts of request. -} module Mig.Core.Class.Plugin ( -- * class ToPlugin (..), Plugin (..), PluginFun, toPlugin, fromPluginFun, ($:), applyPlugin, RawResponse (..), -- * specific plugins prependServerAction, appendServerAction, processResponse, whenSecure, processNoResponse, ) where import Control.Monad.IO.Class import Data.OpenApi (ToParamSchema (..), ToSchema (..)) import Data.Proxy import Data.String import Data.Text (Text) import GHC.TypeLits import Web.FormUrlEncoded (FromForm) import Web.HttpApiData import Mig.Core.Class.MediaType import Mig.Core.Class.Monad import Mig.Core.Class.Response import Mig.Core.Server import Mig.Core.ServerFun import Mig.Core.Types -- | Low-level plugin function. type PluginFun m = ServerFun m -> ServerFun m {-| Plugin can convert all routes of the server. It is wrapper on top of @ServerFun m -> ServerFun m@. We can apply plugins to servers with @applyPlugin@ function also plugin has Monoid instance which is like Monoid.Endo or functional composition @(.)@. -} data Plugin m = Plugin { info :: RouteInfo -> RouteInfo -- ^ update api schema , run :: PluginFun m -- ^ run the plugin } instance Monoid (Plugin m) where mempty = Plugin id id instance Semigroup (Plugin m) where (<>) a b = Plugin (a.info . b.info) (a.run . b.run) -- | Infix operator for @applyPlugin@ ($:) :: forall f. (ToPlugin f) => f -> Server (MonadOf f) -> Server (MonadOf f) ($:) = applyPlugin -- | Applies plugin to all routes of the server. applyPlugin :: forall f. (ToPlugin f) => f -> Server (MonadOf f) -> Server (MonadOf f) applyPlugin a = mapRouteInfo (toPluginInfo @f) . mapServerFun (toPluginFun a) {-| Values that can represent a plugin. We use various newtype-wrappers to query type-safe info from request. -} class (MonadIO (MonadOf f)) => ToPlugin f where toPluginInfo :: RouteInfo -> RouteInfo toPluginFun :: f -> ServerFun (MonadOf f) -> ServerFun (MonadOf f) -- | Convert plugin-like value to plugin. toPlugin :: forall f. (ToPlugin f) => f -> Plugin (MonadOf f) toPlugin a = Plugin (toPluginInfo @f) (toPluginFun a) -- identity instance (MonadIO m) => ToPlugin (PluginFun m) where toPluginInfo = id toPluginFun = id instance (MonadIO m) => ToPlugin (Plugin m) where toPluginInfo = id toPluginFun = (.run) fromPluginFun :: (MonadIO m) => PluginFun m -> Plugin m fromPluginFun = toPlugin -- path info instance (ToPlugin a) => ToPlugin (PathInfo -> a) where toPluginInfo = id toPluginFun f = \fun -> withPathInfo (\path -> toPluginFun (f (PathInfo path)) fun) -- full path info instance (ToPlugin a) => ToPlugin (FullPathInfo -> a) where toPluginInfo = id toPluginFun f = \fun -> withFullPathInfo (\path -> toPluginFun (f (FullPathInfo path)) fun) -- is secure instance (ToPlugin a) => ToPlugin (IsSecure -> a) where toPluginInfo = id toPluginFun f = \fun -> \req -> (toPluginFun (f (IsSecure req.isSecure)) fun) req instance (ToPlugin a) => ToPlugin (RawRequest -> a) where toPluginInfo = id toPluginFun f = \fun -> \req -> (toPluginFun (f (RawRequest req)) fun) req -- | Read low-level response. Note that it does not affect the API schema newtype RawResponse = RawResponse (Maybe Response) instance (ToPlugin a) => ToPlugin (RawResponse -> a) where toPluginInfo = id toPluginFun f = \fun -> \req -> do resp <- fun req (toPluginFun (f (RawResponse resp)) fun) req -- request body instance (FromReqBody ty a, ToSchema a, ToPlugin b) => ToPlugin (Body ty a -> b) where toPluginInfo = addBodyInfo @ty @a . toPluginInfo @b toPluginFun f = \fun -> withBody @ty (\body -> toPluginFun (f (Body body)) fun) -- header instance (FromHttpApiData a, ToParamSchema a, ToPlugin b, KnownSymbol sym) => ToPlugin (Header sym a -> b) where toPluginInfo = addHeaderInfo @sym @a . toPluginInfo @b toPluginFun f = \fun -> withHeader (getName @sym) (\a -> toPluginFun (f (Header a)) fun) -- optional header instance (FromHttpApiData a, ToParamSchema a, ToPlugin b, KnownSymbol sym) => ToPlugin (OptionalHeader sym a -> b) where toPluginInfo = addOptionalHeaderInfo @sym @a . toPluginInfo @b toPluginFun f = \fun -> withOptionalHeader (getName @sym) (\a -> toPluginFun (f (OptionalHeader a)) fun) -- cookie instance (FromForm a, ToPlugin b) => ToPlugin (Cookie a -> b) where toPluginInfo = addOptionalHeaderInfo @"Cookie" @Text . toPluginInfo @b toPluginFun f = \fun -> withCookie (\a -> toPluginFun (f (Cookie a)) fun) -- query instance (FromHttpApiData a, ToParamSchema a, ToPlugin b, KnownSymbol sym) => ToPlugin (Query sym a -> b) where toPluginInfo = addQueryInfo @sym @a . toPluginInfo @b toPluginFun f = \fun -> withQuery (getName @sym) (\a -> toPluginFun (f (Query a)) fun) -- optional query instance (FromHttpApiData a, ToParamSchema a, ToPlugin b, KnownSymbol sym) => ToPlugin (Optional sym a -> b) where toPluginInfo = addOptionalInfo @sym @a . toPluginInfo @b toPluginFun f = \fun -> withOptional (getName @sym) (\a -> toPluginFun (f (Optional a)) fun) -- capture instance (FromHttpApiData a, ToParamSchema a, ToPlugin b, KnownSymbol sym) => ToPlugin (Capture sym a -> b) where toPluginInfo = addCaptureInfo @sym @a . toPluginInfo @b toPluginFun f = \fun -> withCapture (getName @sym) (\a -> toPluginFun (f (Capture a)) fun) -- query flag instance (ToPlugin b, KnownSymbol sym) => ToPlugin (QueryFlag sym -> b) where toPluginInfo = addQueryFlagInfo @sym . toPluginInfo @b toPluginFun f = \fun -> withQueryFlag (getName @sym) (\a -> toPluginFun (f (QueryFlag a)) fun) --------------------------------------------- -- specific plugins -- | Prepends action to the server prependServerAction :: forall m. (MonadIO m) => m () -> Plugin m prependServerAction act = toPlugin go where go :: ServerFun m -> ServerFun m go f = \req -> do act f req -- | Post appends action to the server appendServerAction :: forall m. (MonadIO m) => m () -> Plugin m appendServerAction act = toPlugin go where go :: ServerFun m -> ServerFun m go f = \req -> do resp <- f req act pure resp -- | Applies transformation to the response processResponse :: forall m. (MonadIO m) => (m (Maybe Response) -> m (Maybe Response)) -> Plugin m processResponse act = toPlugin go where go :: ServerFun m -> ServerFun m go f = \req -> do act (f req) -- | Execute request only if it is secure (made with SSL connection) whenSecure :: forall m. (MonadIO m) => Plugin m whenSecure = toPlugin $ \(IsSecure isSecure) -> processResponse (if isSecure then id else const (pure Nothing)) -- | Sets default response if server response with Nothing. If it can not handle the request. processNoResponse :: forall m a. (MonadIO m, IsResp a) => m a -> Plugin m processNoResponse defaultResponse = toPlugin go where go :: PluginFun m go fun = \req -> do mResp <- fun req case mResp of Just resp -> pure (Just resp) Nothing -> Just . toResponse <$> defaultResponse --------------------------------------------- -- utils getName :: forall sym a. (KnownSymbol sym, IsString a) => a getName = fromString (symbolVal (Proxy @sym))