#![cfg(all(
feature = "memory",
feature = "macros",
feature = "json",
feature = "testing"
))]
mod common;
use common::{Order, Receipt};
use ruststream::memory::prelude::*;
use ruststream::runtime::{
ContextKind, ForReply, Names, Outgoing, PublishContext, PublishTransform, Reads,
};
use ruststream::testing::TestApp;
fn reply_to(name: &'static str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("reply-to", name);
headers
}
struct ReplyTo;
impl<C, Options> PublishTransform<ForReply<C>, Options> for ReplyTo {
type Destination = Names;
fn apply(
&self,
out: &mut Outgoing<'_>,
_options: &mut Option<Options>,
cx: &PublishContext<'_, C>,
) {
if let Some(to) = cx.headers().get_shared("reply-to")
&& let Ok(to) = Str::try_from(to)
{
out.set_name(to);
}
}
}
struct Stamp;
impl<K: ContextKind, Options> PublishTransform<K, Options> for Stamp {
type Destination = Reads;
fn apply(&self, out: &mut Outgoing<'_>, _options: &mut Option<Options>, _cx: &K::View<'_>) {
let destination = out.name().as_bytes().to_vec();
out.headers_mut().insert("x-destination", destination);
}
}
#[subscriber("redirect.requests", reply("redirect.receipts"))]
async fn confirm(order: &Order) -> Receipt {
Receipt { id: order.id }
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_naming_transform_that_sets_nothing_leaves_the_declared_destination() {
let app = RustStream::new(AppInfo::new("redirect-fallback", "0.1.0")).with_broker(
MemoryBroker::new(),
|b| {
b.include(confirm).out(Reply, Publish).transform(ReplyTo);
},
);
let tb = TestApp::start(app).await.expect("harness start");
tb.message(&Order { id: 5 })
.to("redirect.requests")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.published::<Receipt>("redirect.receipts")
.assert_called_once()
.with(&Receipt { id: 5 });
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_chain_runs_in_the_order_the_caller_wrote() {
let app = RustStream::new(AppInfo::new("redirect-order", "0.1.0")).with_broker(
MemoryBroker::new(),
|b| {
b.include(confirm)
.out(Reply, Publish)
.transform(ReplyTo)
.transform(Stamp);
},
);
let tb = TestApp::start(app).await.expect("harness start");
tb.message(&Order { id: 6 })
.with_headers(reply_to("redirect.inbox.6"))
.to("redirect.requests")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.published::<Receipt>("redirect.inbox.6")
.assert_called_once()
.with_header("x-destination", b"redirect.inbox.6");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn without_one_the_declared_destination_stands() {
let app = RustStream::new(AppInfo::new("redirect-none", "0.1.0")).with_broker(
MemoryBroker::new(),
|b| {
b.include(confirm).out(Reply, Publish).transform(Stamp);
},
);
let tb = TestApp::start(app).await.expect("harness start");
tb.message(&Order { id: 7 })
.with_headers(reply_to("redirect.inbox.7"))
.to("redirect.requests")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.published::<Receipt>("redirect.receipts")
.assert_called_once()
.with_header("x-destination", b"redirect.receipts");
}
struct ByTenant;
impl<K: ContextKind, Options> PublishTransform<K, Options> for ByTenant {
type Destination = Names;
fn apply(&self, out: &mut Outgoing<'_>, _options: &mut Option<Options>, _cx: &K::View<'_>) {
if let Some(tenant) = out.headers().get("x-tenant")
&& let Ok(tenant) = std::str::from_utf8(tenant)
{
out.set_name(format!("redirect.audit.{tenant}"));
}
}
}
#[derive(OutSlot)]
#[publishes(Order)]
struct Audit;
#[subscriber("redirect.orders")]
async fn mirror(order: &Order, Out(audit): Out<impl Publisher, Audit>) -> HandlerOutcome {
let mut headers = HeaderMap::new();
headers.insert("x-tenant", "north");
let sent = audit
.message(order)
.with_headers(headers)
.to("redirect.audit")
.publish()
.await;
if sent.is_err() {
return HandlerOutcome::retry();
}
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_slot_publishes_where_its_naming_transform_says() {
let app = RustStream::new(AppInfo::new("redirect-slot", "0.1.0")).with_broker(
MemoryBroker::new(),
|b| {
b.include(mirror)
.out(Audit, Publish)
.transform(ByTenant)
.build();
},
);
let tb = TestApp::start(app).await.expect("harness start");
tb.message(&Order { id: 9 })
.to("redirect.orders")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.published::<Order>("redirect.audit.north")
.assert_called_once()
.with(&Order { id: 9 });
let audited = tb.out::<Audit>().assert_called_once();
assert_eq!(audited.messages()[0].name(), "redirect.audit.north");
}
#[subscriber("redirect.mixed", reply("redirect.mixed.receipts"))]
async fn confirm_and_audit(order: &Order, Out(audit): Out<impl Publisher, Audit>) -> Receipt {
let _ = audit.message(order).to("redirect.audit").publish().await;
Receipt { id: order.id }
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_naming_transform_rides_the_reply_of_a_slot_carrying_handler() {
let app = RustStream::new(AppInfo::new("redirect-mixed", "0.1.0")).with_broker(
MemoryBroker::new(),
|b| {
b.include(confirm_and_audit)
.out(Reply, Publish)
.transform(ReplyTo)
.out(Audit, Publish)
.build();
},
);
let tb = TestApp::start(app).await.expect("harness start");
tb.message(&Order { id: 12 })
.with_headers(reply_to("redirect.inbox.12"))
.to("redirect.mixed")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.published::<Receipt>("redirect.inbox.12")
.assert_called_once()
.with(&Receipt { id: 12 });
}
#[subscriber("redirect.plain")]
async fn plain(order: &Order, Out(out): Out<impl Publisher, DefaultSlot>) -> HandlerOutcome {
if out
.message(order)
.to("redirect.plain.out")
.publish()
.await
.is_err()
{
return HandlerOutcome::retry();
}
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_marker_without_a_dictionary_still_takes_an_ordinary_transform() {
let app = RustStream::new(AppInfo::new("redirect-default-slot", "0.1.0")).with_broker(
MemoryBroker::new(),
|b| {
b.include(plain)
.out(DefaultSlot, Publish)
.transform(Stamp)
.build();
},
);
let tb = TestApp::start(app).await.expect("harness start");
tb.message(&Order { id: 13 })
.to("redirect.plain")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.published::<Order>("redirect.plain.out")
.assert_called_once()
.with_header("x-destination", b"redirect.plain.out");
}