Plugins
A plugin is a transformation which is applied to all routes in the server.
Also often it is called a middleware. But here we use a bit shorter name for it
and call it a Plugin
.
It is a pair of functions which transform API-description and server function:
data Plugin m = Plugin
{ info :: RouteInfo -> RouteInfo
-- ^ update api schema
, run :: PluginFun m
-- ^ update server function
}
-- | Low-level plugin function.
type PluginFun m = ServerFun m -> ServerFun m
To apply plugin to server we ca use function applyPlugin
:
-- | Applies plugin to all routes of the server.
applyPlugin :: forall f. (ToPlugin f) =>
f -> Server (MonadOf f) -> Server (MonadOf f)
There is also infix operator for application ($:)
.
The class ToPlugin
contains all types that can be converted to plugin.
Here we use the same trick as with ToServer
class to be able to read type-safe parts of the request
and update the API-schema. The type-level function MonadOf
knows how to find underlying monad m
in various types.
We have recursive set of rules for types that can be converted to Plugin
:
The identity rule:
PluginFun
has instance ofToPlugin
with obvious identity instance
Recursive steps for inputs
if
f
isToPlugin
then(Query name queryType -> f)
isToPlugin
too
And so on for other types of request input (query parameters, headers, captures, request bodies).
See the full list of instances in the module Mig.Core.Class.Plugin
.
Examples
So the plugin allows us to apply some behavior to all routes in the server. Let's discuss some examples
Add logging
Let's add the logging to all methods which are called. We will log which route was called and we will include the time stamp and the full path in the log:
Let's imagine that we have a function
logInfo :: Text -> IO ()
We can query the path with FullPathInfo
newtype
:
newtype FullPathInfo = FullPathInfo Text
And we have a rule for ToPlugin
class:
if
f
isToPlugin
then(FullPathInfo -> ToPlugin f)
isToPlugin
So we can create a plugin function:
logRoutes :: Plugin IO
logRoutes = toPlugin $ \(FullPathInfo path) -> prependServerAction $ do
now <- getCurrentTime
logInfo $ mconcat
[ "Call route: ", path
, " at ", Text.pack (show now)
]
We use function prependServerAction
that creates a Plugin
from action which is performed prior to call to server function:
prependServerAction :: MonadIO m => m () -> Plugin m
Also there are similar functions in the module: appendServerAction
and processResponse
.
Allow only secure routes
Another great example of plugin at work is to block routes on some conditions.
For example if we want certain routes to be used only under secure SSL connection.
We have a standard function for that whenSecure
. But let's dive into it's definition to
see how plugins can be used:
-- | 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))
Here we use standard plugin processResponse
which allows
us to alter the result of the HTTP-response:
processResponse :: MonadIO m =>
(m (Maybe Response) -> m (Maybe Response)) -> Plugin m
Also we use query input IsSecure
which is true if connection is made over SSL:
newtype IsSecure = IsSecure Bool
So we pass through the response with identity if connection is secure
and we block the execution by returning Nothing
if connection is secure.
The cool part of it is that due to laziness there is no performance overhead and underlying
route is not going to be performed if connection is insecure.
Authorization with plugin
Let's use this schema for authorization to site. There is a route that provides authorized users with session tokens. A user can pass credentials as request body over secure connection and get session token in response which is valid for some time.
With that token the user can access the rest of the application. The user can pass token as a special header. And we check in the application that token is valid.
Imagine that we have a type for a session token:
newtype AuthToken = AuthToken Text
deriving newtype
(ToJSON, FromJSON, FromHttpApiData, Eq, Ord, Show, ToParamSchema, ToSchema)
And we can get it from some route:
getToken :: Body UserCreds -> Post (Resp AuthToken)
We would like to block invalid sessions for all routes of our site.
We can create it in similar way as whenSecure
:
isValid :: AuthToken -> IO Bool
isValid = ...
headerAuth :: Header "auth" AuthToken -> Plugin IO
headerAuth (Header token) = processResponse $ \getResp -> do
isOk <- isValid token
if isOk
then getResp
else pure $ Just $ bad badRequest400 "Auth token is invalid"
whenAuth :: Server IO -> Server IO
whenAuth = applyPlugin headerAuth
In this example we use IsResp
instance for low-level http Response
to report authorization error. The header with name "auth"
is required
for all routes which are part of the server to which we apply the plugin.
Summary
In this chapter we have learned on plugins. They provide a tool to apply transformation to all routes in the server. Which can be useful for logging, authorization and adding common behavior to all routes.