#![cfg_attr(not(test), allow(dead_code, unused_imports))]
use ruststream::memory::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize, schemars::JsonSchema)]
struct Order {
id: u64,
}
impl OutgoingDestination for Order {
type Form = FixedName;
const DESTINATION: &'static str = "orders";
}
impl MessageHeaders for Order {
type Contract = NoHeaders;
}
mod start {
use std::convert::Infallible;
use std::error::Error;
use std::future::{Future, ready};
use ruststream::memory::prelude::*;
use ruststream::testing::TestApp;
use super::Order;
struct Blocklist {
refused: Vec<u64>,
}
struct Screen;
impl Handle<Order, (), (), (), Blocklist> for Screen {
fn handle(
&self,
order: &Order,
_outs: &(),
ctx: &mut Context<'_, (), Blocklist>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
if ctx.state().refused.contains(&order.id) {
return ready(Err(HandlerOutcome::drop()));
}
ready(Ok(()))
}
}
fn app() -> impl App<State = Blocklist> {
RustStream::new(AppInfo::new("orders", "0.1.0"))
.on_startup(async move |()| Ok::<_, Infallible>(Blocklist { refused: vec![13] }))
.with_broker(MemoryBroker::new(), |b| {
b.include(subscriber("orders", Screen).build());
})
}
#[tokio::test]
async fn a_refused_order_is_dropped() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.broker::<MemoryBroker>()
.message(&Order { id: 13 })
.publish()
.await?;
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called_once()
.settled(HandlerOutcome::drop());
Ok(())
}
}
mod start_live {
use std::error::Error;
use std::future::{Future, ready};
use std::time::Duration;
use ruststream::memory::prelude::*;
use ruststream::testing::TestApp;
use super::Order;
struct Accept;
impl Handle<Order> for Accept {
fn handle(
&self,
order: &Order,
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
if order.id == 0 {
return ready(Err(HandlerOutcome::drop()));
}
ready(Ok(()))
}
}
fn app() -> impl App<State = ()> {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(subscriber("orders", Accept).build());
})
}
async fn accepts_an_order(tb: TestApp<()>) -> Result<(), Box<dyn Error>> {
tb.broker::<MemoryBroker>()
.message(&Order { id: 7 })
.publish()
.await?;
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called_once()
.settled(HandlerOutcome::ack());
tb.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn in_process() -> Result<(), Box<dyn Error>> {
accepts_an_order(TestApp::start(app()).await?).await
}
#[tokio::test]
async fn against_the_stand() -> Result<(), Box<dyn Error>> {
accepts_an_order(TestApp::start_live(app()).await?).await
}
#[tokio::test]
async fn against_a_distant_stand() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start_live_within(app(), Duration::from_secs(30)).await?;
accepts_an_order(tb).await
}
}
mod with_state {
use std::collections::HashMap;
use std::error::Error;
use ruststream::memory::MemoryPublisher;
use ruststream::memory::prelude::*;
use ruststream::testing::TestApp;
use serde::{Deserialize, Serialize};
use super::Order;
#[derive(Debug, PartialEq, Deserialize, Serialize, schemars::JsonSchema)]
struct Quote {
order_id: u64,
cents: u32,
}
impl OutgoingDestination for Quote {
type Form = FixedName;
const DESTINATION: &'static str = "quotes";
}
impl MessageHeaders for Quote {
type Contract = NoHeaders;
}
#[derive(Debug, thiserror::Error)]
#[error("the catalog is unreachable")]
struct CatalogError;
struct Catalog {
prices: HashMap<u64, u32>,
}
impl Catalog {
async fn load(url: &str) -> Result<Self, CatalogError> {
let _ = url;
tokio::task::yield_now().await;
Err(CatalogError)
}
}
struct Pricing {
catalog: Catalog,
quotes: MemoryPublisher,
}
struct Price;
impl Handle<Order, (), (), (), Pricing> for Price {
async fn handle(
&self,
order: &Order,
_outs: &(),
ctx: &mut Context<'_, (), Pricing>,
) -> Result<(), HandlerOutcome> {
let pricing = ctx.state();
let Some(¢s) = pricing.catalog.prices.get(&order.id) else {
return Err(HandlerOutcome::drop());
};
let quote = Quote {
order_id: order.id,
cents,
};
if pricing.quotes.message("e).publish().await.is_err() {
return Err(HandlerOutcome::retry());
}
Ok(())
}
}
fn app() -> impl App<State = Pricing> {
let broker = MemoryBroker::new();
let quotes = broker.publisher();
RustStream::new(AppInfo::new("pricing", "0.1.0"))
.on_startup(async move |()| {
let catalog = Catalog::load("http://catalog.internal").await?;
Ok::<_, CatalogError>(Pricing { catalog, quotes })
})
.with_broker(broker, |b| {
b.include(subscriber("orders", Price).build());
})
}
#[tokio::test]
async fn an_order_is_quoted_from_the_catalog() -> Result<(), Box<dyn Error>> {
let tb = TestApp::with_state(app(), |brokers| Pricing {
catalog: Catalog {
prices: HashMap::from([(7, 1250)]),
},
quotes: brokers.broker::<MemoryBroker>().publisher(),
})
.await?;
tb.broker::<MemoryBroker>()
.message(&Order { id: 7 })
.publish()
.await?;
tb.broker::<MemoryBroker>()
.published::<Quote>("quotes")
.assert_called_once()
.with(&Quote {
order_id: 7,
cents: 1250,
});
Ok(())
}
}
mod shutdown {
use std::error::Error;
use std::future::{Future, ready};
use ruststream::memory::prelude::*;
use ruststream::runtime::RustStreamError;
use ruststream::testing::TestApp;
use super::Order;
struct Fulfil;
impl Handle<Order> for Fulfil {
fn handle(
&self,
order: &Order,
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
assert_ne!(order.id, 0, "an order without an id reached fulfilment");
ready(Ok(()))
}
}
fn app() -> impl App<State = ()> {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(subscriber("orders", Fulfil).build());
})
}
#[tokio::test]
async fn a_healthy_run_shuts_down_cleanly() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.broker::<MemoryBroker>()
.message(&Order { id: 7 })
.publish()
.await?;
tb.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn a_panic_is_reported_at_shutdown() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.broker::<MemoryBroker>()
.message(&Order { id: 0 })
.publish()
.await?;
assert!(matches!(
tb.shutdown().await,
Err(RustStreamError::Dispatch(_))
));
Ok(())
}
}
fn main() {}