use std::convert::Infallible;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use ruststream::memory::{MemoryBroker, MemoryMessage};
use ruststream::runtime::{AppInfo, Ctx, HandlerResult, RustStream, State};
use ruststream::testing::TestApp;
use ruststream::{BuildContext, ContextField, Field, FromRef, IncomingMessage, subscriber};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Order {
id: u64,
}
struct DeliveryMeta {
payload_len: usize,
}
impl BuildContext<MemoryMessage> for DeliveryMeta {
fn build(msg: &MemoryMessage) -> Self {
Self {
payload_len: msg.payload().len(),
}
}
}
#[derive(Clone, Copy, Default)]
struct PayloadLen;
impl ContextField for PayloadLen {
type Context = DeliveryMeta;
type Value = usize;
fn read(self, src: &DeliveryMeta) -> usize {
src.payload_len
}
}
impl Field<DeliveryMeta> for PayloadLen {
type Value<'a> = usize;
fn get(self, src: &DeliveryMeta) -> usize {
src.payload_len
}
}
static SEEN_LEN: AtomicUsize = AtomicUsize::new(0);
#[subscriber("orders")]
async fn measure(_order: &Order, Ctx(len): Ctx<PayloadLen>) -> HandlerResult {
SEEN_LEN.store(len, Ordering::Relaxed);
HandlerResult::Ack
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn ctx_extractor_projects_the_context_from_its_key() {
let app = RustStream::new(AppInfo::new("orders", "0.1.0"))
.with_broker(MemoryBroker::new(), |b| b.include(measure));
let tb = TestApp::start(app).await.expect("start");
tb.broker::<MemoryBroker>()
.publish("orders", &Order { id: 7 })
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called_once()
.settled(HandlerResult::Ack);
let expected = serde_json::to_vec(&Order { id: 7 }).expect("encode").len();
assert_eq!(
SEEN_LEN.load(Ordering::Relaxed),
expected,
"the extracted field must come from this delivery's context",
);
}
#[subscriber("mixed")]
async fn both(
_order: &Order,
ctx: &mut Context<'_, DeliveryMeta>,
Ctx(len): Ctx<PayloadLen>,
) -> HandlerResult {
assert_eq!(
ctx.context(PayloadLen),
len,
"the extractor and the key read must agree",
);
HandlerResult::Ack
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn ctx_extractor_composes_with_an_explicit_ctx_parameter() {
let app = RustStream::new(AppInfo::new("orders", "0.1.0"))
.with_broker(MemoryBroker::new(), |b| b.include(both));
let tb = TestApp::start(app).await.expect("start");
tb.broker::<MemoryBroker>()
.publish("mixed", &Order { id: 1 })
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.subscriber("mixed")
.assert_called_once()
.settled(HandlerResult::Ack);
}
#[derive(Clone)]
struct Hits(Arc<AtomicU32>);
#[derive(FromRef)]
struct AppState {
hits: Hits,
}
#[subscriber("counted")]
async fn count(
_order: &Order,
Ctx(len): Ctx<PayloadLen>,
State(hits): State<Hits>,
) -> HandlerResult {
if len > 0 {
hits.0.fetch_add(1, Ordering::Relaxed);
}
HandlerResult::Ack
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn ctx_extractor_composes_with_state() {
let hits = Arc::new(AtomicU32::new(0));
let state_hits = Hits(hits.clone());
let app = RustStream::new(AppInfo::new("orders", "0.1.0"))
.on_startup(move |()| async move { Ok::<_, Infallible>(AppState { hits: state_hits }) })
.with_broker(MemoryBroker::new(), |b| b.include(count));
let tb = TestApp::start(app).await.expect("start");
tb.broker::<MemoryBroker>()
.publish("counted", &Order { id: 2 })
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.subscriber("counted")
.assert_called_once()
.settled(HandlerResult::Ack);
assert_eq!(hits.load(Ordering::Relaxed), 1);
}