#![cfg_attr(not(test), allow(dead_code, unused_imports))]
use std::future::{Future, ready};
use ruststream::HeaderMap;
use ruststream::memory::prelude::*;
use ruststream::runtime::{BlanketLayer, Handler, Layer};
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize, schemars::JsonSchema)]
struct Order {
id: u64,
}
impl OutgoingDestination for Order {
type Form = CallerName;
}
impl MessageHeaders for Order {
type Contract = NoHeaders;
}
struct Log;
impl Handle<Order> for Log {
fn handle(
&self,
order: &Order,
_outs: &(),
ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
tracing::info!(order.id, subscription = ctx.name(), "handled");
ready(Ok(()))
}
}
fn tenant(name: &'static str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("tenant", name);
headers
}
#[derive(Clone)]
struct RequireTenant;
struct Tenanted<H>(H);
impl<H> Layer<H> for RequireTenant {
type Handler = Tenanted<H>;
fn layer(&self, inner: H) -> Tenanted<H> {
Tenanted(inner)
}
}
impl<M, C, S, H> Handler<M, C, S> for Tenanted<H>
where
M: Send + Sync,
C: Send,
S: Send + Sync,
H: Handler<M, C, S>,
{
async fn handle(&self, msg: &M, ctx: &mut Context<'_, C, S>) -> HandlerOutcome {
if ctx.headers().get("tenant").is_none() {
tracing::warn!(subscription = ctx.name(), "no tenant header, dropped");
return HandlerOutcome::drop();
}
self.0.handle(msg, ctx).await
}
}
impl BlanketLayer for RequireTenant {
fn apply<M, C, S, H>(&self, handler: H) -> impl Handler<M, C, S> + 'static
where
M: Send + Sync + 'static,
C: Send + 'static,
S: Send + Sync + 'static,
H: Handler<M, C, S> + 'static,
{
Tenanted(handler)
}
}
mod app_wide {
use std::error::Error;
use ruststream::memory::prelude::*;
use ruststream::testing::{Outcome, TestApp};
use super::{Log, Order, RequireTenant, tenant};
fn app() -> impl App {
RustStream::new(AppInfo::new("orders", "0.1.0"))
.layer(RequireTenant)
.with_broker(MemoryBroker::new(), |b| {
b.include(subscriber("orders", Log).build());
})
}
#[tokio::test]
async fn an_order_without_a_tenant_is_dropped() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
let broker = tb.broker::<MemoryBroker>();
let order = Order { id: 7 };
broker.message(&order).to("orders").publish().await?;
broker
.message(&order)
.with_headers(tenant("acme"))
.to("orders")
.publish()
.await?;
let outcomes = broker.subscriber("orders").outcomes();
assert_eq!(outcomes, [Outcome::Drop, Outcome::Ack]);
Ok(())
}
}
mod scopes {
use std::error::Error;
use ruststream::memory::prelude::*;
use ruststream::testing::TestApp;
use super::{Log, Order, RequireTenant};
fn app() -> impl App {
RustStream::new(AppInfo::new("billing", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include_router(
Router::new()
.layer(RequireTenant)
.include(subscriber("refunds", Log).build()),
);
b.include_router(
Router::new()
.include(subscriber("invoices", Log).build())
.layer(RequireTenant)
.include(subscriber("audit", Log).build()),
);
})
}
#[tokio::test]
async fn only_the_wrapped_handlers_drop_an_untenanted_order() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
let broker = tb.broker::<MemoryBroker>();
let order = Order { id: 7 };
for channel in ["refunds", "invoices", "audit"] {
broker.message(&order).to(channel).publish().await?;
}
broker.subscriber("refunds").settled(HandlerOutcome::drop());
broker
.subscriber("invoices")
.settled(HandlerOutcome::drop());
broker.subscriber("audit").settled(HandlerOutcome::ack());
Ok(())
}
}
fn main() {}