Skip to content

Commit

Permalink
Merge pull request #15 from anton-k/rename-middleware
Browse files Browse the repository at this point in the history
Rename middleware
  • Loading branch information
anton-k authored Oct 19, 2023
2 parents 0a2879e + daa3716 commit a019c2b
Show file tree
Hide file tree
Showing 26 changed files with 704 additions and 781 deletions.
26 changes: 13 additions & 13 deletions docs/src/02-request-anatomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,46 +407,46 @@ is fine.
### Add simple logs to the server

We can look at the request and trsponse data with tracing functions
which come from library `mig-extra` from the module `Mig.Extra.Middleware.Trace`:
which come from library `mig-extra` from the module `Mig.Extra.Plugin.Trace`:

```haskell
data Verbosity = V0 | V1 | V2 | V3

-- log http requests and responses
logHttp :: Verbosity -> Middleware m
logHttp :: Verbosity -> Plugin m

-- | log requests
logReq :: Verbosity -> Middleware m
logReq :: Verbosity -> Plugin m

-- | Log responses
logResp :: Verbosity -> Middleware m
logResp :: Verbosity -> Plugin m
```

The `Middleware m` is a function that can be applied to all routes of the server
and modify their behavior. To apply middleware to server we can use functions:
The `Plugin m` is a function that can be applied to all routes of the server
and modify their behavior. To apply plugin to server we can use functions:

```haskell
applyMiddleware :: Middleware m -> Server m -> Server m
applyPlugin :: Plugin m -> Server m -> Server m

($:) :: Middleware m -> Server m -> Server m
($:) :: Plugin m -> Server m -> Server m
```

We show simplified signatures here. The real ones are overloaded by the first argument.
but we will dicuss middlewares in depth in the separate chapter. For now it's
but we will dicuss plugins in depth in the separate chapter. For now it's
ok to assume that those functions are defined in that simplified way.

So let's look at the data that goes through our server:

```haskell
import Mig.Extra.Middleware.Trace qualified as Trace
import Mig.Extra.Plugin.Trace qualified as Trace

...

server =
withSwagger def $
withTrace $ {-# the rest of the server code #-}
where
withTrace = applyMiddleware (Trace.logHttp Trace.V2)
withTrace = applyPlugin (Trace.logHttp Trace.V2)
```

Let's restart the server and see what it logs:
Expand Down Expand Up @@ -484,7 +484,7 @@ of one of the standard haskell logging libraries, say `katip` or `fast-logger`:
```haskell
import Data.Aeson as Json
logHttpBy :: (Json.Value -> m ()) -> Verbosity -> Middleware m
logHttpBy :: (Json.Value -> m ()) -> Verbosity -> Plugin m
```

## Summary
Expand All @@ -510,7 +510,7 @@ we have learned how by ony-liners we can add to the server some useful features:
* swagger: `(withSwagger def server)`
For calls to the server in the UI

* trace logs: `(applyMiddleware (logHttp V2))`
* trace logs: `(applyPlugin (logHttp V2))`
To see the data that flows through the server

Both expressions transform servers and have signatures:
Expand Down
66 changes: 33 additions & 33 deletions docs/src/05-middleware.md → docs/src/05-plugin.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,51 @@
# Middlewares
# Plugins

A middleware is a transformation which is applied to all routes in the server.
A plugin is a transformation which is applied to all routes in the server.
It is a pair of functions which transform API-description and server function:

```haskell
data Middleware m = Middleware
data Plugin m = Plugin
{ info :: RouteInfo -> RouteInfo
-- ^ update api schema
, run :: MiddlewareFun m
, run :: PluginFun m
-- ^ update server function
}

-- | Low-level middleware function.
type MiddlewareFun m = ServerFun m -> ServerFun m
-- | Low-level plugin function.
type PluginFun m = ServerFun m -> ServerFun m
```

To apply middleware to server we ca use function `applyMiddleware`:
To apply plugin to server we ca use function `applyPlugin`:

```haskell
-- | Applies middleware to all routes of the server.
applyMiddleware :: forall f. (ToMiddleware f) =>
-- | Applies plugin to all routes of the server.
applyPlugin :: forall f. (ToPlugin f) =>
f -> Server (MonadOf f) -> Server (MonadOf f)
```

There is also infix operatore for application `($:)`.

The class `ToMiddleware` contains all types that can be converted to middleware.
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 `Middleware`:
We have recursive set of rules for types that can be converted to `Plugin`:

The identity rule:

> `MiddlewareFun` has instance of `ToMiddleware` with obvious identity instance
> `PluginFun` has instance of `ToPlugin` with obvious identity instance

Recursive steps for inputs

> if `f` is `ToMiddleware` then `(Query name queryType -> f)` is `ToMiddleware` too
> if `f` is `ToPlugin` then `(Query name queryType -> f)` is `ToPlugin` too

and so on for other types of request input (query params, headers, captures, request bodies).
See the full list of instances in the module `Mig.Core.Class.Middleware`.
See the full list of instances in the module `Mig.Core.Class.Plugin`.

## Examples

So the middleware allows us to apply some behavior to all routes in the server.
So the plugin allows us to apply some behavior to all routes in the server.
Let's discuss some examples

### Add logging
Expand All @@ -65,51 +65,51 @@ We can query the path with `PathInfo` newtype:
newtype PathInfo = PathInfo [Text]
```

And we have a rule for `ToMiddleware` class:
And we have a rule for `ToPlugin` class:

> if `f` is `ToMiddleware` then `(PathInfo -> ToMiddleware f)` is `ToMiddleware`
> if `f` is `ToPlugin` then `(PathInfo -> ToPlugin f)` is `ToPlugin`

so we can create a middleware function:
so we can create a plugin function:

```haskell
logRoutes :: Middleware IO
logRoutes = toMiddleware $ \(PathInfo pathItems) -> prependServerAction $ do
logRoutes :: Plugin IO
logRoutes = toPlugin $ \(PathInfo pathItems) -> prependServerAction $ do
now <- getCurrentTime
logInfo $ mconcat
[ "Call route: ", Text.intercalata "/" pathItems
, " at ", Text.pack (show now)
]
```

We use function `prependServerAction` that creates a `Middleware`
We use function `prependServerAction` that creates a `Plugin`
from actino which is performed prior to call to server function:

```haskell
prependServerAction :: MonadIO m => m () -> Middleware m
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 middleware at work is to block routes on some conditions.
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 middlewares can be used:
see how plugins can be used:

```haskell
-- | Execute request only if it is secure (made with SSL connection)
whenSecure :: forall m. (MonadIO m) => Middleware m
whenSecure = toMiddleware $ \(IsSecure isSecure) ->
whenSecure :: forall m. (MonadIO m) => Plugin m
whenSecure = toPlugin $ \(IsSecure isSecure) ->
processResponse (if isSecure then id else const (pure Nothing))
```

Here we use standard middleware `processResponse` which allows
Here we use standard plugin `processResponse` which allows
us to alter the result of the HTTP-response:

```haskell
processResponse :: MonadIO m =>
(m (Maybe Response) -> m (Maybe Response)) -> Middleware m
(m (Maybe Response) -> m (Maybe Response)) -> Plugin m
```

Also we use query input `IsSecure` which is true if connection is made over SSL:
Expand All @@ -123,7 +123,7 @@ and we block the execution by returning `Nothing` if connection is secure.
The cool part of it is that due to Haskell's laziness there is no performance overhead and underlying
route is not going to be performed if connection is insecure.

### Authorization with middleware
### Authorization with plugin

Let's use this schema for authorization to site.
There is a route that provides authorized users with session tokens.
Expand Down Expand Up @@ -155,23 +155,23 @@ We can create it in similiar way as `whenSecure`:
isValid :: AuthToken -> IO Bool
isValid = ...

headerAuth :: Header "auth" AuthToken -> Middleware IO
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 = applyMiddleware headerAuth
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 middleware.
for all routes which are part of the server to which we apply the plugin.

## Summary

In this chapter we have learned on middlewares. They provide a tool to apply
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, authrization
and adding common behavior to all routes.
12 changes: 6 additions & 6 deletions docs/src/06-json-api-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ updateWeather = undefined
requestAuthToken :: Env -> Body User -> Post (RespOr Text AuthToken)
requestAuthToken = undefined

withAuth :: Env -> Header "auth" AuthToken -> Middleware IO
withAuth :: Env -> Header "auth" AuthToken -> Plugin IO
withAuth = undefined
```

Expand All @@ -160,10 +160,10 @@ updateWeather ::
Post (RespOr Text ())
```

also we have a middleware that filters out non aunthorized calls:
also we have a plugin that filters out non aunthorized calls:

```haskell
withAuth :: Env -> Header "auth" AuthToken -> Middleware IO
withAuth :: Env -> Header "auth" AuthToken -> Plugin IO
```

From its type-signature we can assume that authroization token
Expand Down Expand Up @@ -353,11 +353,11 @@ that hnadles the request. If user has no rights to use our service we report err
Let's check for authorization tokens. Ideally we would like to add
this action to all handlers of our application. We would like to keep
the business logic handlers for the weather domain the same.
And we can do it with middleware. Let's define such a middleware
And we can do it with plugin. Let's define such a plugin
that expects authorization tokens with required header:

```haskell
withAuth :: Env -> Header "auth" AuthToken -> Middleware IO
withAuth :: Env -> Header "auth" AuthToken -> Plugin IO
withAuth env (Header token) = processResponse $ \getResp -> do
isOk <- env.auth.validToken token
if isOk
Expand All @@ -369,7 +369,7 @@ withAuth env (Header token) = processResponse $ \getResp -> do
errMessage = "Token is invalid"
```

we have covered in depth how to implement it in the chapter on Middlewares
we have covered in depth how to implement it in the chapter on Plugins
so this code should look familiar to us.


Expand Down
26 changes: 13 additions & 13 deletions docs/src/08-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,16 @@ runServer' :: ServerConfig -> Int -> Server IO -> IO ()
### Request inputs

```haskell
-- rewquired query parameter
-- required query parameter
newtype Body media value = Body value

-- rewquired query parameter
-- required query parameter
newtype Query name value = Query value

-- optional query parameter
newtype Optional name value = Optional (Maybe value)

-- rewquired header parameter
-- required header parameter
newtype Header name value = Header value

-- optional header parameter
Expand Down Expand Up @@ -149,13 +149,13 @@ notImplemented :: (IsResp a) => RespError a -> a
redirect :: (IsResp a) => Text -> a
```

### Middlewares
### Plugins

```haskell
applyMiddleware, ($:) :: ToMiddleware a =>
applyPlugin, ($:) :: ToPlugin a =>
a -> Server (MonadOf a) -> Server (MonadOf a)

-- composition of middlewares:
-- composition of plugins:
Monoid(..): mconcat, (<>), mempty
```

Expand All @@ -172,27 +172,27 @@ addPathLink :: Path -> Path -> Server m
staticFiles :: [(FilePath, ByteString)] -> Server m
```

### specific middlewares
### specific plugins


```haskell
-- prepend or append some acction to all routes
prependServerAction, appendServerAction :: MonadIO m => m () -> Middleware m
prependServerAction, appendServerAction :: MonadIO m => m () -> Plugin m

-- change the response
processResponse :: (m (Maybe Response) -> m (Maybe Response)) -> Middleware m
processResponse :: (m (Maybe Response) -> m (Maybe Response)) -> Plugin m

-- only secure routes are allowed
whenSecure :: forall m. (MonadIO m) => Middleware m
whenSecure :: forall m. (MonadIO m) => Plugin m

-- logging with putStrLn for debug traces
logHttp :: Verbosity -> Middleware m
logHttp :: Verbosity -> Plugin m

-- logging with custom logger
logHttpBy :: (Json.Value -> m ()) -> Verbosity -> Middleware m
logHttpBy :: (Json.Value -> m ()) -> Verbosity -> Plugin m

-- | simple authorization
withHeaderAuth :: WithHeaderAuth -> Middleware m
withHeaderAuth :: WithHeaderAuth -> Plugin m
```

### How to use Reader
Expand Down
2 changes: 1 addition & 1 deletion docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- [Anatomy of the request](./02-request-anatomy.md)
- [Anatomy of the response](./03-response-anatomy.md)
- [Using other monads](./04-other-monads.md)
- [Middlewares](./05-middleware.md)
- [Plugins](./05-plugin.md)
- [Using Swagger](./06-swagger.md)
- [JSON application: weather forecast](./06-json-api-example.md)
- [HTML example: blog site](./07-blog-post-example.md)
Expand Down
2 changes: 1 addition & 1 deletion examples/mig-example-apps/Html/src/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ server site =
]

logRoutes :: Server IO -> Server IO
logRoutes = applyMiddleware $ \(PathInfo path) -> prependServerAction $
logRoutes = applyPlugin $ \(PathInfo path) -> prependServerAction $
when (path /= ["favicon.ico"] && headMay path /= Just "static") $ do
logRoute site (Text.intercalate "/" path)

Expand Down
Loading

0 comments on commit a019c2b

Please sign in to comment.