#![cfg(all(
feature = "memory",
feature = "macros",
feature = "json",
feature = "testing"
))]
use ruststream::OutgoingFor;
#[cfg(feature = "cbor")]
use ruststream::codec::{CborCodec, Codec};
use ruststream::memory::prelude::*;
use ruststream::testing::{TestApp, TestableBroker};
use serde::{Deserialize, Serialize};
#[derive(Debug, Outgoing, PartialEq, Serialize, Deserialize)]
struct Job {
id: u64,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct DoneMeta {
task_id: u64,
}
#[derive(Debug, PartialEq, Outgoing, Serialize, Deserialize)]
#[outgoing(name = "chunks.progress")]
struct Progress {
percent: u8,
}
#[derive(Debug, PartialEq, Outgoing, Serialize, Deserialize)]
#[outgoing(name = "chunks.done", headers = DoneMeta)]
struct ChunkDone {
output_key: String,
}
#[derive(Debug, PartialEq, Outgoing, Serialize, Deserialize)]
#[outgoing(name = "orders.{tenant}.{region}.v1")]
struct OrderPlaced {
id: u64,
}
#[derive(Debug, PartialEq, Outgoing, Serialize, Deserialize)]
struct OrderArchived {
id: u64,
}
#[derive(Outgoing, Serialized)]
struct Wire(Vec<u8>);
impl Wire {
fn of(bytes: impl AsRef<[u8]>) -> Self {
Self(bytes.as_ref().to_vec())
}
}
#[derive(OutSlot)]
#[publishes(ChunkDone, Progress, OrderPlaced, OrderArchived)]
struct Events;
#[derive(OutSlot)]
#[publishes(Wire)]
struct Frames;
#[subscriber("jobs.in")]
async fn convert(
job: &Job,
Out(out): Out<impl Publisher, Events, (ChunkDone, Progress, OrderPlaced, OrderArchived)>,
Out(frames): Out<impl Publisher, Frames, Wire>,
) -> HandlerOutcome {
if out
.message(&Progress { percent: 50 })
.publish()
.await
.is_err()
{
return HandlerOutcome::retry();
}
let done = ChunkDone {
output_key: format!("out/{}", job.id),
};
if out
.message(&done)
.with_headers(&DoneMeta { task_id: job.id })
.publish()
.await
.is_err()
{
return HandlerOutcome::retry();
}
if out
.message(&OrderPlaced { id: job.id })
.to()
.tenant("acme")
.region(7_u32)
.publish()
.await
.is_err()
{
return HandlerOutcome::retry();
}
if out
.message(&OrderArchived { id: job.id })
.to("orders.archived")
.publish()
.await
.is_err()
{
return HandlerOutcome::retry();
}
let mut headers = HeaderMap::new();
headers.insert("source", "jobs.in");
if frames
.message(&Wire::of(b"frame"))
.with_headers(headers)
.to("chunks.raw")
.publish()
.await
.is_err()
{
return HandlerOutcome::retry();
}
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn every_destination_form_resolves_through_one_builder() {
let app =
RustStream::new(AppInfo::new("builder", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(convert)
.out(Events, Publish)
.out(Frames, Publish)
.build();
});
let tb = TestApp::start(app).await.expect("harness start");
tb.message(&Job { id: 3 })
.to("jobs.in")
.publish()
.await
.expect("publish");
let broker = tb.broker::<MemoryBroker>();
broker
.published::<Progress>("chunks.progress")
.assert_called_once()
.with(&Progress { percent: 50 });
broker
.published::<OrderPlaced>("orders.acme.7.v1")
.assert_called_once()
.with(&OrderPlaced { id: 3 });
broker
.published::<OrderArchived>("orders.archived")
.assert_called_once()
.with(&OrderArchived { id: 3 });
let done = broker
.published::<ChunkDone>("chunks.done")
.assert_called_once();
assert_eq!(done.messages()[0].headers().get_str("task_id"), Some("3"));
let raw = tb.out::<Frames>();
let framed = raw
.messages()
.iter()
.find(|msg| msg.name() == "chunks.raw")
.expect("the raw publish is attributed to the slot");
assert_eq!(framed.payload(), b"frame");
assert_eq!(framed.headers().get_str("source"), Some("jobs.in"));
}
fn replaying() -> MemoryBroker<Retaining> {
MemoryBroker::retaining(Retention::Messages(nonzero!(64)))
}
#[cfg(feature = "cbor")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_call_names_its_own_codec() {
let broker = replaying();
let connected = broker.clone().connect().await.expect("connect");
connected
.publisher()
.message(&OrderArchived { id: 9 })
.with_codec(CborCodec)
.to("orders.archived".to_owned())
.publish()
.await
.expect("call-level codec and a computed name");
let archived = connected.published("orders.archived");
let decoded: OrderArchived = CborCodec
.decode(archived[0].payload())
.expect("the payload is in the codec the call named");
assert_eq!(decoded, OrderArchived { id: 9 });
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_publisher_and_its_transactions_carry_the_builder() {
let broker = replaying();
let connected = broker.clone().connect().await.expect("connect");
let publisher = connected.publisher();
publisher
.message(&Progress { percent: 10 })
.publish()
.await
.expect("bare publisher");
let scope = publisher.begin().await.expect("begin");
scope
.message(&Progress { percent: 20 })
.publish()
.await
.expect("in transaction");
scope
.message(&Wire::of(b"trace"))
.to("audit.trail")
.publish()
.await
.expect("carried bytes in transaction");
scope.commit().await.expect("commit");
publisher
.message(&Wire::of(b"wire"))
.to("audit.wire")
.publish()
.await
.expect("carried bytes through the bare publisher");
let mut owned = publisher
.owned_transaction()
.await
.expect("owned transaction");
owned
.message(&OrderArchived { id: 1 })
.to("orders.archived")
.publish()
.await
.expect("in owned transaction");
owned
.message(&Wire::of(b"ledger"))
.to("audit.ledger")
.publish()
.await
.expect("carried bytes in owned transaction");
owned.commit().await.expect("commit");
assert_eq!(connected.published("chunks.progress").len(), 2);
assert_eq!(connected.published("audit.trail").len(), 1);
assert_eq!(connected.published("audit.wire").len(), 1);
assert_eq!(connected.published("orders.archived").len(), 1);
assert_eq!(connected.published("audit.ledger").len(), 1);
}
#[test]
fn a_publish_builder_hides_its_wiring() {
let publisher = MemoryBroker::new().publisher();
let pending = publisher.message(&Progress { percent: 1 });
assert_eq!(format!("{pending:?}"), "PublishBuilder { .. }");
}
struct Tenanted<P>(P, HeaderMap);
impl<P: Publisher> Publisher for Tenanted<P> {
type Payload = P::Payload;
type Error = P::Error;
type Options = P::Options;
async fn publish(
&self,
msg: OutgoingFor<'_, Self::Payload>,
options: Option<&Self::Options>,
) -> Result<(), Self::Error> {
self.0.publish(msg, options).await
}
fn base_headers(&self) -> Option<&HeaderMap> {
Some(&self.1)
}
}
impl<P: OwnedTransactions> OwnedTransactions for Tenanted<P> {
type Transaction = Tagged<P::Transaction>;
async fn transaction(&self) -> Result<Self::Transaction, Self::Error> {
Ok(Tagged(self.0.transaction().await?, self.1.clone()))
}
}
struct Tagged<T>(T, HeaderMap);
impl<T: Transaction> Transaction for Tagged<T> {
type Payload = T::Payload;
type Error = T::Error;
type Options = T::Options;
async fn publish(
&mut self,
msg: OutgoingFor<'_, Self::Payload>,
options: Option<&Self::Options>,
) -> Result<(), Self::Error> {
self.0.publish(msg, options).await
}
async fn commit(self) -> Result<(), Self::Error> {
self.0.commit().await
}
async fn abort(self) -> Result<(), Self::Error> {
self.0.abort().await
}
fn base_headers(&self) -> Option<&HeaderMap> {
Some(&self.1)
}
}
fn tenant_base() -> HeaderMap {
[("tenant", "acme"), ("x-trace", "handle")]
.into_iter()
.collect()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_publisher_without_a_base_sends_only_the_call_sites_headers() {
let broker = replaying();
let connected = broker.clone().connect().await.expect("connect");
let mut headers = HeaderMap::new();
headers.insert("x-trace", "call");
connected
.publisher()
.message(&Wire::of(b"bytes"))
.to("audit")
.with_headers(headers)
.publish()
.await
.expect("map headers without a base");
connected
.publisher()
.message(&Wire::of(b"bytes"))
.to("audit.bare")
.publish()
.await
.expect("no headers at all");
let sent = connected.published("audit");
assert_eq!(sent[0].headers().get_str("x-trace"), Some("call"));
assert_eq!(
sent[0].headers().len(),
1,
"a publisher with no base adds nothing of its own",
);
assert_eq!(
*connected.published("audit.bare")[0].headers(),
HeaderMap::new()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_call_site_wins_over_the_handles_base_key_by_key() {
let broker = replaying();
let connected = broker.clone().connect().await.expect("connect");
let publisher = Tenanted(connected.publisher(), tenant_base());
publisher
.message(&Progress { percent: 1 })
.publish()
.await
.expect("the base alone");
let mut headers = HeaderMap::new();
headers.insert("x-trace", "call");
headers.insert("x-request-id", "r-1");
publisher
.message(&Progress { percent: 2 })
.with_headers(headers)
.publish()
.await
.expect("a map over the base");
let sent = connected.published("chunks.progress");
assert_eq!(sent[0].headers().get_str("tenant"), Some("acme"));
assert_eq!(sent[0].headers().get_str("x-trace"), Some("handle"));
let merged = sent[1].headers();
assert_eq!(
merged.get_str("x-trace"),
Some("call"),
"the call site has the last word on a key both name",
);
assert_eq!(
merged.get_str("tenant"),
Some("acme"),
"a base key the call does not name survives",
);
assert_eq!(merged.get_str("x-request-id"), Some("r-1"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_header_contract_serializes_over_the_handles_base() {
let broker = replaying();
let connected = broker.clone().connect().await.expect("connect");
let mut base = tenant_base();
base.insert("task_id", "0");
let publisher = Tenanted(connected.publisher(), base);
publisher
.message(&ChunkDone {
output_key: "out/1".to_owned(),
})
.with_headers(&DoneMeta { task_id: 7 })
.publish()
.await
.expect("a contract over the base");
let sent = connected.published("chunks.done");
let headers = sent[0].headers();
assert_eq!(
headers.get_str("task_id"),
Some("7"),
"the contract field overwrites the base's placeholder",
);
assert_eq!(headers.get_str("tenant"), Some("acme"));
assert_eq!(headers.get_str("x-trace"), Some("handle"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_transaction_carries_its_own_base_under_the_call_site() {
let broker = replaying();
let connected = broker.clone().connect().await.expect("connect");
let publisher = Tenanted(connected.publisher(), tenant_base());
let mut txn = publisher
.owned_transaction()
.await
.expect("owned transaction");
txn.message(&Progress { percent: 3 })
.publish()
.await
.expect("the base alone, buffered");
let mut headers = HeaderMap::new();
headers.insert("x-trace", "call");
txn.message(&Wire::of(b"ledger"))
.to("audit.ledger")
.with_headers(headers)
.publish()
.await
.expect("a map over the base, buffered");
txn.commit().await.expect("commit");
let progress = connected.published("chunks.progress");
assert_eq!(progress[0].headers().get_str("tenant"), Some("acme"));
assert_eq!(progress[0].headers().get_str("x-trace"), Some("handle"));
let ledger = connected.published("audit.ledger");
assert_eq!(ledger[0].headers().get_str("x-trace"), Some("call"));
assert_eq!(ledger[0].headers().get_str("tenant"), Some("acme"));
}