messua/layer
A Layer is a Handler function with another “inner” handler (which
itself might be another layer). This is the natural format for
“middleware”.
The layer function may pre-process or alter the request before passing it to the inner handler; it may process or alter the response returned from the inner handler; it may also choose to generate its own response and not even call the inner handler.
The stack() function can be used to compose multiple layers; here’s
an example which may help convey the structure.
Dramatis functionae:
handler(in: Incoming(state)) -> Outgoing: the innermost handler function, which handles the buisness logicblacklist(in: Incoming(state), inner: Handler(state)) -> Outgoing: a blacklist layer function, which outrightly rejects requests coming from certain addressesauth(in: Incoming(state), inner: Handler(state)) -> Outgoing: an authentication layer, which checks for appropriate credentials and rejects requests which do not have them
But in this example, we will just set each up to log a message to the web server’s console, so we can see in which order things happen.
import messua/handle.{type Handler}
import messua/minc.{type Incoming}
import messua/mout.{type Outgoing}
fn blacklist(in: Incoming(Nil), inner: Handler(Nil)) -> Outgoing {
io.println("blacklist layer receives Incoming")
let out = inner(in)
io.println("blacklist layer returns Outgoing")
out
}
fn auth(in: Incoming(Nil), inner: Handler(Nil)) -> Outgoing {
io.println("auth layer receives Incoming")
let out = inner(in)
io.println("auth layer returns Outgoing")
out
}
fn handler(_in: Incoming(Nil)) -> Outgoing {
io.println("handler processes Incoming and returns Outgoing")
mout.ok()
}
We set up and listen with our handler stack function thus:
pub fn main() {
let handler_stack = layer.stack(handler, [auth, blacklist])
messua.default()
|> messua.start(handler_stack)
}
And when the server fields a request, it prints this out to the console:
blacklist layer receives Incoming
auth layer receives Incoming
handler processes Incoming and returns Outgoing
auth layer returns Outgoing
blacklist layer returns Outgoing
So you can see the layered structure of the request handler is such:
| ^
Incoming Outgoing
v |
----------------------------------
| blacklist() |
| pre-handling post-handling |
----------------------------------
| ^
Incoming Outgoing
v |
----------------------------------
| auth() |
| pre-handling post-handling |
----------------------------------
| ^
Incoming Outgoing
v |
----------------------------------
| | handler() ^ |
| | | |
| ---> innermost logic --- |
----------------------------------
And of course, at any point a layer could choose to return a response
without passing the incoming request to the layer below. To simulate
that, we rewrite our auth() function to simulate authentication
failure.
fn auth(_in: Incoming(Nil), _inner: Handler(Nil)) -> Outgoing {
io.println("auth layer receives Incoming")
io.println("authentication error!")
mout.fail()
|> mout.with_status(403)
}
So the innermost handler function never gets called:
blacklist layer receives Incoming
auth layer receives Incoming
authentication error!
blacklist layer returns Outgoing
Types
A handler function with its own innner handler function. The Layer
is free to pass the (possibly modified) Incoming request to this
inner function, or generate a response of its own.
pub type Layer(state) =
fn(
minc.Incoming(state),
fn(minc.Incoming(state)) -> Result(
response.Response(mist.ResponseData),
fail.Failure,
),
) -> Result(response.Response(mist.ResponseData), fail.Failure)
Values
pub fn make_blacklist_layer(
addrs: List(addr.IpAddr),
) -> fn(
minc.Incoming(state),
fn(minc.Incoming(state)) -> Result(
response.Response(mist.ResponseData),
fail.Failure,
),
) -> Result(response.Response(mist.ResponseData), fail.Failure)
Return a Layer that will automatically reject (with a 404) any
request from any of the specified addrs.
This is just the simplest thing that could function, and doesn’t make any affordances for specifying IP ranges or looking at any other feature of the incoming request.
pub fn make_static_header_layer(
headers: List(#(String, String)),
) -> fn(
minc.Incoming(state),
fn(minc.Incoming(state)) -> Result(
response.Response(mist.ResponseData),
fail.Failure,
),
) -> Result(response.Response(mist.ResponseData), fail.Failure)
Return a Layer that will append a series of #(name, value) headers
to any outgoing reply.
pub fn stack(
base: fn(minc.Incoming(state)) -> Result(
response.Response(mist.ResponseData),
fail.Failure,
),
layers: List(
fn(
minc.Incoming(state),
fn(minc.Incoming(state)) -> Result(
response.Response(mist.ResponseData),
fail.Failure,
),
) -> Result(
response.Response(mist.ResponseData),
fail.Failure,
),
),
) -> fn(minc.Incoming(state)) -> Result(
response.Response(mist.ResponseData),
fail.Failure,
)
Combine a stack of Layers and an innermost Handler function into
a single, combined, handler function.
The ordering of the layers list is such that the last function in
the list is the first to receive the incoming request, and the last
to return the outgoing response.
pub fn update_incoming(
in: minc.Incoming(state),
update: fn(request.Request(mist.Connection)) -> request.Request(
mist.Connection,
),
) -> minc.Incoming(state)
Update or alter the incoming request.