froodi
froodi (crates.io, docs.rs, GitHub) is a dependency-injection container whose ruststream feature builds a handler's dependencies per delivery, as Inject parameters.
The registry declares each dependency with the scope it lives in.
Config lives in the App scope, as long as the container.
Pricing lives in the Request scope and is built from Config by its factory.
ContainerLayer is a middleware, mounted with RustStream::layer: for every delivery it enters a child Request scope, runs the handler inside it and closes the scope once the handler has answered.
Closing the scope runs the finalizers of what it built before the message is settled.
Inject<Pricing> is a FromContext extractor that resolves from that child scope, so two parameters of one delivery share an instance.
InjectTransient<T> builds a fresh instance for each parameter instead.
A dependency the container cannot resolve drops the delivery and logs the channel and the type.
A handler that takes Inject in a service with no ContainerLayer drops every delivery the same way.
The layer builds a child container for every delivery, which allocates; dependencies in the App scope are built once.
With froodi's async feature, AsyncContainerLayer holds a container whose factories and finalizers can await.
Example
- Macros
- Manual
[dependencies]
froodi = { version = "1.0.0-beta.19", features = ["ruststream"] }
ruststream = { version = "0.7", features = ["macros", "memory", "json"] }
serde = { version = "1", features = ["derive"] }
use froodi::ruststream::ContainerLayer;
use froodi::{Container, DefaultScope, Inject, instance, registry};
use ruststream::memory::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
pub struct Order {
pub id: u64,
pub items: u64,
}
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "receipts")]
pub struct Receipt {
pub order_id: u64,
pub total: u64,
}
/// Lives as long as the application.
#[derive(Clone)]
pub struct Config {
pub price_per_item: u64,
}
/// Built for every delivery from the configuration.
pub struct Pricing {
per_item: u64,
}
impl Pricing {
fn total(&self, order: &Order) -> u64 {
order.items * self.per_item
}
}
fn container(config: Config) -> Container {
Container::new(registry! {
provide(DefaultScope::App, instance(config)),
scope(DefaultScope::Request) [
provide(|Inject(config): Inject<Config>| Ok(Pricing {
per_item: config.price_per_item,
})),
],
})
}
#[subscriber("orders", reply)]
async fn price(order: &Order, Inject(pricing): Inject<Pricing>) -> Receipt {
Receipt {
order_id: order.id,
total: pricing.total(order),
}
}
pub fn app() -> impl App {
RustStream::new(AppInfo::new("orders", "0.1.0"))
.layer(ContainerLayer::new(container(Config {
price_per_item: 25,
})))
.with_broker(MemoryBroker::new(), |b| {
b.include(price).out_reply(Publish);
})
}
[dependencies]
froodi = { version = "1.0.0-beta.19", features = ["ruststream"] }
ruststream = { version = "0.7", default-features = false, features = ["memory", "json"] }
schemars = "1"
serde = { version = "1", features = ["derive"] }
use froodi::ruststream::ContainerLayer;
use froodi::{Container, DefaultScope, Inject, instance, registry};
use ruststream::memory::prelude::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct Order {
pub id: u64,
pub items: u64,
}
// `#[derive(Outgoing)]` with no `name`, by hand: the call site names the destination.
impl OutgoingDestination for Order {
type Form = CallerName;
}
impl MessageHeaders for Order {
type Contract = NoHeaders;
}
#[derive(Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct Receipt {
pub order_id: u64,
pub total: u64,
}
// `#[outgoing(name = "receipts")]` by hand: the type fixes its destination.
impl OutgoingDestination for Receipt {
type Form = FixedName;
const DESTINATION: &'static str = "receipts";
}
impl MessageHeaders for Receipt {
type Contract = NoHeaders;
}
/// Lives as long as the application.
#[derive(Clone)]
pub struct Config {
pub price_per_item: u64,
}
/// Built for every delivery from the configuration.
pub struct Pricing {
per_item: u64,
}
impl Pricing {
fn total(&self, order: &Order) -> u64 {
order.items * self.per_item
}
}
fn container(config: Config) -> Container {
Container::new(registry! {
provide(DefaultScope::App, instance(config)),
scope(DefaultScope::Request) [
provide(|Inject(config): Inject<Config>| Ok(Pricing {
per_item: config.price_per_item,
})),
],
})
}
struct Price;
impl Handle<Order, Receipt> for Price {
async fn handle(
&self,
order: &Order,
_outs: &(),
ctx: &mut Context<'_>,
) -> Result<Receipt, HandlerOutcome> {
// What the attribute emits for `Inject(pricing): Inject<Pricing>`: a failed resolution
// settles the delivery by its outcome and the body does not run.
let Inject(pricing) = <Inject<Pricing> as FromContext>::from_context(ctx)
.await
.map_err(HandlerOutcome::from)?;
Ok(Receipt {
order_id: order.id,
total: pricing.total(order),
})
}
}
pub fn app() -> impl App {
RustStream::new(AppInfo::new("orders", "0.1.0"))
.layer(ContainerLayer::new(container(Config {
price_per_item: 25,
})))
.with_broker(MemoryBroker::new(), |b| {
b.include(subscriber("orders", Price).reply().build())
.out_reply(Publish);
})
}