use std::fmt;
use std::future::{Future, ready};
use std::marker::PhantomData;
use tracing::warn;
use crate::{Broker, Connected, IncomingMessage, PairError};
use super::context::Context;
use super::dispatch::Workers;
use super::failure::{FailurePolicies, FailurePolicy};
use super::handler::{Handler, HandlerOutcome};
use super::input::{DecodeWith, InputKind};
use super::metadata::{HandlerMetadata, OutgoingMessageMetadata};
use super::slot::DefaultSlot;
#[derive(Debug)]
pub struct Out<P, M = DefaultSlot, Body = ()>(OutVocabulary<P, M, Body>);
type OutVocabulary<P, M, Body> = PhantomData<fn() -> (P, M, Body)>;
pub(crate) trait FromStartup<B: Broker, Sub, Extra>: Sized {
fn resolve(
extra: Extra,
connected: &Connected<B>,
subscriber: &Sub,
) -> impl Future<Output = Result<Self, PairError>> + Send;
}
impl<B: Broker, Sub: Sync, Extra: Send> FromStartup<B, Sub, Extra> for () {
fn resolve(
_extra: Extra,
_connected: &Connected<B>,
_subscriber: &Sub,
) -> impl Future<Output = Result<Self, PairError>> {
ready(Ok(()))
}
}
macro_rules! impl_from_startup_for_tuples {
($(($($name:ident: $extra:ident),+))+) => {$(
impl<B, Sub, $($name, $extra),+> FromStartup<B, Sub, ($($extra,)+)> for ($($name,)+)
where
B: Broker,
Sub: Sync,
$(
$name: FromStartup<B, Sub, $extra> + Send,
$extra: Send,
)+
{
async fn resolve(
extra: ($($extra,)+),
connected: &Connected<B>,
subscriber: &Sub,
) -> Result<Self, PairError> {
#[allow(non_snake_case)]
let ($($extra,)+) = extra;
Ok(($($name::resolve($extra, connected, subscriber).await?,)+))
}
}
)+};
}
impl_from_startup_for_tuples! {
(T1: E1)
(T1: E1, T2: E2)
(T1: E1, T2: E2, T3: E3)
(T1: E1, T2: E2, T3: E3, T4: E4)
}
pub trait InjectDef: Send + Sync {
type Input: InputKind;
type Context;
type Source;
type Injections;
fn source(&self) -> Self::Source;
fn workers(&self) -> Workers {
Workers::sequential()
}
fn failure_policies(&self) -> FailurePolicies {
FailurePolicies::default()
}
fn description(&self) -> Option<&str> {
None
}
fn input_schema(&self) -> Option<String> {
None
}
fn headers_schema(&self) -> Option<String> {
None
}
fn outgoing(&self) -> Vec<OutgoingMessageMetadata> {
Vec::new()
}
fn message_name(&self) -> Option<&'static str> {
None
}
fn message_description(&self) -> Option<&'static str> {
None
}
}
pub(crate) trait InjectCall<S>: InjectDef {
fn call(
&self,
input: &<Self::Input as InputKind>::Target,
injections: &Self::Injections,
ctx: &mut Context<'_, Self::Context, S>,
) -> impl Future<Output = HandlerOutcome> + Send;
}
pub(crate) fn inject_metadata<D: InjectDef>(name: String, def: &D) -> HandlerMetadata {
let mut meta = HandlerMetadata::raw(name).with_def_details(
def.description(),
def.input_schema(),
def.headers_schema(),
def.message_name(),
def.message_description(),
);
meta.input_type = <D::Input as InputKind>::input_label();
meta.deserialized = <D::Input as InputKind>::DESERIALIZED;
meta.outgoing = def.outgoing();
meta
}
pub(crate) struct InjectHandler<Def: InjectDef, DecodeCodec> {
pub(crate) def: Def,
pub(crate) codec: DecodeCodec,
pub(crate) injections: Def::Injections,
pub(crate) decode: FailurePolicy,
}
impl<Def: InjectDef, DecodeCodec> fmt::Debug for InjectHandler<Def, DecodeCodec> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InjectHandler").finish_non_exhaustive()
}
}
impl<Msg, Def, DecodeCodec, State> Handler<Msg, Def::Context, State>
for InjectHandler<Def, DecodeCodec>
where
Msg: IncomingMessage,
Def: InjectCall<State>,
Def::Input: DecodeWith<DecodeCodec>,
Def::Context: Send + Sync,
Def::Injections: Send + Sync,
DecodeCodec: Send + Sync,
State: Send + Sync,
{
async fn handle(
&self,
msg: &Msg,
ctx: &mut Context<'_, Def::Context, State>,
) -> HandlerOutcome {
let owned = match <Def::Input as DecodeWith<DecodeCodec>>::decode(&self.codec, msg) {
Ok(value) => value,
Err(err) => {
warn!(
target: "ruststream::dispatch",
subscription = %ctx.name(),
message_type = <Def::Input as InputKind>::input_label(),
error = %err,
"codec decode failed",
);
#[cfg(any(feature = "testing", feature = "otel"))]
ctx.mark_decode_failed();
return match self.decode {
FailurePolicy::FailFast => {
ctx.fail_fast(&format!("decode failed: {err}"));
HandlerOutcome::drop()
}
other => other
.settlement()
.map_or_else(HandlerOutcome::drop, Into::into),
};
}
};
let view = <Def::Input as InputKind>::view(&owned, msg.payload());
self.def.call(view, &self.injections, ctx).await
}
}
#[cfg(test)]
mod tests {
use std::future::ready;
use super::{InjectCall, InjectDef, InjectHandler};
use crate::Name;
use crate::runtime::context::Context;
use crate::runtime::dispatch::Workers;
use crate::runtime::failure::{FailurePolicies, FailurePolicy};
use crate::runtime::handler::HandlerOutcome;
use crate::runtime::input::Decoded;
struct ManualInject;
impl InjectDef for ManualInject {
type Input = Decoded<u32>;
type Context = ();
type Source = Name;
type Injections = ();
fn source(&self) -> Name {
Name::new("in")
}
}
impl<S: Send + Sync> InjectCall<S> for ManualInject {
fn call(
&self,
input: &u32,
(): &(),
_ctx: &mut Context<'_, (), S>,
) -> impl Future<Output = HandlerOutcome> {
let _ = *input;
ready(HandlerOutcome::ack())
}
}
#[test]
fn the_defaults_declare_nothing_the_macro_would_have_filled_in() {
let def = ManualInject;
assert_eq!(def.workers(), Workers::sequential());
assert_eq!(def.failure_policies(), FailurePolicies::default());
assert!(def.description().is_none());
assert!(def.input_schema().is_none());
assert!(def.headers_schema().is_none());
assert!(def.outgoing().is_empty());
assert!(def.message_name().is_none());
assert!(def.message_description().is_none());
assert!(format!("{:?}", def.source()).contains("in"));
}
#[test]
fn the_handler_names_itself() {
let handler = InjectHandler {
def: ManualInject,
codec: (),
injections: (),
decode: FailurePolicy::Drop,
};
assert!(format!("{handler:?}").contains("InjectHandler"));
}
#[cfg(all(feature = "memory", feature = "json", feature = "logging"))]
mod diagnostics {
use futures::StreamExt;
use super::ManualInject;
use crate::codec::JsonCodec;
use crate::memory::{MemoryBroker, MemoryMessage};
use crate::runtime::context::Context;
use crate::runtime::dispatch::Delivery;
use crate::runtime::failure::FailurePolicy;
use crate::runtime::handler::Handler;
use crate::runtime::inject::InjectHandler;
use crate::testkit::log_capture::{find, start};
use crate::{HeaderMap, OutgoingMessage, Publisher, Subscriber};
async fn one_delivery(broker: &MemoryBroker, name: &str, payload: &[u8]) -> MemoryMessage {
let mut subscriber = broker.subscribe(name);
broker
.publisher()
.publish(OutgoingMessage::new(name, payload), None)
.await
.expect("publish failed");
let mut stream = std::pin::pin!(subscriber.stream());
stream
.next()
.await
.expect("delivery missing")
.expect("memory subscriber never errors")
}
#[tokio::test]
async fn a_decode_failure_names_the_subscription_and_the_expected_type() {
let broker = MemoryBroker::new();
let msg = one_delivery(&broker, "in", b"not json").await;
let handler = InjectHandler {
def: ManualInject,
codec: JsonCodec,
injections: (),
decode: FailurePolicy::FailFast,
};
let state = ();
let delivery = Delivery::empty();
let headers = HeaderMap::new();
let mut ctx = Context::new("in", &headers, &state, (), &delivery);
let (events, guard) = start();
let settle = handler.handle(&msg, &mut ctx).await;
drop(guard);
let failure = find(&events, "codec decode failed");
assert_eq!(failure.get("subscription").map(String::as_str), Some("in"));
assert_eq!(failure.get("message_type").map(String::as_str), Some("u32"));
assert!(
failure
.get("error")
.is_some_and(|err| err.contains("decode")),
"the diagnostic must carry the codec error: {failure:?}",
);
assert!(settle.is_drop());
}
}
}