Typed requests, events and middleware for the Moon-Lander framework
graph LR
C[Caller] -->|ctx, req| A[Middleware A]
A -->|next| B[Middleware B]
B -->|next| H[Handler]
H -->|reply| B
B -->|reply| A
A -->|reply| C| Part | Value | Make it with |
|---|---|---|
| Sender | Sender[C, Req, Res]: one handler for one request type | @mediator.sender(handler) |
| Topic | Topic[C, Ev]: one or more handlers for one event type | @mediator.topic(handler), then add |
| Middleware | Middleware[C, Req, Res]: one wrapper step | @mediator.middleware(wrap), before, after, finally_ |
| Error | MediatorError: a refusal, or the failures of a publish | raise @mediator.MediatorError::rejected(...); publish raises PublishFailed |
| Type parameter | Meaning |
|---|---|
| C | The context value that the caller gives to each send |
| Req | The request type. A request of another type does not compile |
| Res | The reply type of the handler |
///|
struct Ping(String)
///|
test "send a ping" {
let ping : @mediator.Sender[Unit, Ping, String] = @mediator.sender((_, req) => {
let Ping(text) = req
"Pong: " + text
})
assert_eq(ping.send((), Ping("Hello")), "Pong: Hello")
}///|
struct Joined(String)
///|
test "publish to two handlers" {
let log = []
let joined : @mediator.Topic[Unit, Joined] = @mediator.topic((_, ev) => {
let Joined(name) = ev
log.push("welcome " + name)
})
let all = joined.add((_, ev) => {
let Joined(name) = ev
log.push("count " + name)
})
all.publish((), Joined("Ann"))
assert_eq(log, ["welcome Ann", "count Ann"])
}| Handlers that raise | Handlers that run | Publish gives |
|---|---|---|
| None | All, in add order | () |
| One or more | All, in add order | PublishFailed(handlers~, failures~) after the last handler |
///|
suberror CountError {
CountFull(String)
}
///|
test "one handler fails and the others still run" {
let log = []
let joined : @mediator.Topic[Unit, Joined] = @mediator.topic((_, ev) => {
let Joined(name) = ev
log.push("welcome " + name)
})
let all = joined
.add((_, ev) => {
let Joined(name) = ev
raise CountFull(name)
})
.add((_, ev) => {
let Joined(name) = ev
log.push("audit " + name)
})
try all.publish((), Joined("Ann")) catch {
@mediator.PublishFailed(handlers~, failures~) as error => {
assert_eq(handlers, 3)
assert_eq(failures.length(), 1)
guard failures.get(0) is Some(failure) else { fail("missing failure") }
assert_eq(failure.position, 2)
assert_true(failure.error is CountFull("Ann"))
assert_eq(
error.message(),
"Publish failed: 1 of 3 handlers raised an error (position 2)",
)
}
other => fail("publish raised another error: \{other}")
} noraise {
_ => fail("publish did not raise")
}
assert_eq(log, ["welcome Ann", "audit Ann"])
}| Constructor | Runs |
|---|---|
| @mediator.middleware(wrap) | wrap(next, ctx, req), which gives the reply |
| @mediator.before(hook) | hook(ctx, req) before the next step |
| @mediator.after(hook) | hook(ctx, req, reply) after the next step gives a reply |
| @mediator.finally_(hook) | hook(ctx, req) after the next step, also when it raises |
///|
test "log middleware around a send" {
let log = []
let ping : @mediator.Sender[Unit, Ping, String] = @mediator.sender((_, req) => {
log.push("handler")
let Ping(text) = req
"Pong: " + text
})
let logged = ping
.use_(
@mediator.middleware((next, ctx, req) => {
log.push("log in")
let reply = next(ctx, req)
log.push("log out")
reply
}),
)
.use_(@mediator.after((_, _, reply) => log.push("reply " + reply)))
assert_eq(logged.send((), Ping("Hello")), "Pong: Hello")
assert_eq(log, ["log in", "handler", "reply Pong: Hello", "log out"])
}///|
struct Services {
greeting : String
}
///|
struct AppContext {
services : Services
}
///|
test "a handler reads the services from the context" {
let ping : @mediator.Sender[AppContext, Ping, String] = @mediator.sender((
ctx,
req,
) => {
let Ping(text) = req
ctx.services.greeting + ": " + text
})
let ctx = { services: { greeting: "Pong", }, }
assert_eq(ping.send(ctx, Ping("Hello")), "Pong: Hello")
}| Case | Raised by | message gives |
|---|---|---|
| Rejected(reason~) | A handler or a middleware that refuses a request, for example after validation | Request rejected: <reason> |
| TimedOut(after~) | Async timeout middleware, with a core TimeLimit | Request timed out after <milliseconds> ms |
| PublishFailed(handlers~, failures~) | publish, when one or more handlers raise | Publish failed: 2 of 3 handlers raised an error (positions 2, 3) |
///|
test "a validation middleware rejects a request" {
let ping : @mediator.Sender[Unit, Ping, String] = @mediator.sender((_, req) => {
let Ping(text) = req
"Pong: " + text
})
let checked = ping.use_(
@mediator.before((_, req) => {
let Ping(text) = req
if text == "" {
raise @mediator.MediatorError::rejected("the ping text is empty")
}
}),
)
try checked.send((), Ping("")) catch {
@mediator.Rejected(_) as error =>
assert_eq(error.message(), "Request rejected: the ping text is empty")
other => fail("send raised another error: \{other}")
} noraise {
reply => fail("send gave a reply: \{reply}")
}
assert_eq(checked.send((), Ping("Hello")), "Pong: Hello")
}///|
test "derive a report and read failures safely" {
let outcomes : Array[Error?] = [None, Some(CountFull("audit"))]
guard @mediator.MediatorError::from_publish_errors(outcomes) is Some(error) else {
fail("expected a publish error")
}
guard error is @mediator.PublishFailed(handlers~, failures~) else {
fail("expected PublishFailed")
}
assert_eq(handlers, 2)
assert_eq(failures.length(), 1)
guard failures.get(0) is Some(failure) else { fail("expected a failure") }
assert_eq(failure.position, 2)
assert_true(failure.error is CountFull("audit"))
assert_true(failures.get(1) is None)
let copy = failures.to_array()
copy.clear()
outcomes.clear()
assert_eq(failures.length(), 1)
}pub suberror MediatorError {
Rejected(reason~ : String)
PublishFailed(handlers~ : Int, failures~ : PublishFailures)
TimedOut(after~ : TimeLimit)
}type Middleware[C, Req, Res]pub struct PublishFailure {
position : Int
error : Error
}type PublishFailurestype Sender[C, Req, Res]fn[C, Req, Res] Sender::use_(self : Sender[C, Req, Res], middleware : Middleware[C, Req, Res]) -> Sender[C, Req, Res]type Topic[C, Ev]fn[C, Req, Res] middleware(wrap : ((C, Req) -> Res raise, C, Req) -> Res raise) -> Middleware[C, Req, Res]