use std::any::type_name;
use std::convert::Infallible;
use std::fmt;
use std::future::Future;
use serde::de::DeserializeOwned;
use tracing::warn;
use crate::ContextField;
use super::context::Context;
use super::failure::FailurePolicy;
use super::handler::HandlerOutcome;
use super::main_runtime::MainRuntime;
pub trait FromContext<C = (), S = ()>: Sized {
type Rejection: Into<HandlerOutcome>;
fn from_context(
ctx: &mut Context<'_, C, S>,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send;
}
pub trait FromRef<S>: Sized {
fn from_ref(state: &S) -> Self;
}
#[derive(Debug, Clone, Copy)]
pub struct State<T>(pub T);
impl<C, S, T> FromContext<C, S> for State<T>
where
T: FromRef<S> + Send,
{
type Rejection = Infallible;
fn from_context(
ctx: &mut Context<'_, C, S>,
) -> impl Future<Output = Result<Self, Infallible>> + Send {
let value = T::from_ref(ctx.state());
async move { Ok(Self(value)) }
}
}
pub struct Ctx<K: CtxKey>(pub K::Value);
pub trait CtxKey: sealed::Sealed {
type Value: Send + 'static;
}
impl<K: ContextField> CtxKey for K {
type Value = K::Value;
}
impl CtxKey for MainRuntime {
type Value = Self;
}
mod sealed {
use super::{ContextField, MainRuntime};
pub trait Sealed {}
impl<K: ContextField> Sealed for K {}
impl Sealed for MainRuntime {}
}
impl<K: CtxKey> fmt::Debug for Ctx<K>
where
K::Value: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Ctx").field(&self.0).finish()
}
}
impl<S, K> FromContext<K::Context, S> for Ctx<K>
where
K: ContextField,
K::Context: Send,
S: Sync,
{
type Rejection = Infallible;
fn from_context(
ctx: &mut Context<'_, K::Context, S>,
) -> impl Future<Output = Result<Self, Infallible>> + Send {
let value = K::default().read(ctx.cx_ref());
async move { Ok(Self(value)) }
}
}
impl<C, S> FromContext<C, S> for Ctx<MainRuntime>
where
C: Send,
S: Sync,
{
type Rejection = Infallible;
fn from_context(
ctx: &mut Context<'_, C, S>,
) -> impl Future<Output = Result<Self, Infallible>> + Send {
let value = ctx.main_runtime().clone();
async move { Ok(Self(value)) }
}
}
#[derive(Debug, Clone, Copy)]
pub struct Headers<T>(pub T);
impl<T: DeserializeOwned> Headers<T> {
#[doc(hidden)]
pub fn extract<C, S>(
ctx: &mut Context<'_, C, S>,
policy: FailurePolicy,
) -> Result<Self, HandlerOutcome> {
match ctx.headers().to_typed::<T>() {
Ok(value) => Ok(Self(value)),
Err(err) => {
warn!(
target: "ruststream::dispatch",
subscription = %ctx.name(),
headers_type = type_name::<T>(),
error = %err,
"typed header extraction failed",
);
#[cfg(any(feature = "testing", feature = "otel"))]
ctx.mark_decode_failed();
Err(match policy {
FailurePolicy::FailFast => {
ctx.fail_fast(&format!("header extraction failed: {err}"));
HandlerOutcome::drop()
}
other => other
.settlement()
.map_or_else(HandlerOutcome::drop, Into::into),
})
}
}
}
}
impl<C, S, T> FromContext<C, S> for Headers<T>
where
T: DeserializeOwned + Send,
C: Send,
S: Sync,
{
type Rejection = HandlerOutcome;
fn from_context(
ctx: &mut Context<'_, C, S>,
) -> impl Future<Output = Result<Self, HandlerOutcome>> + Send {
let result = Self::extract(ctx, FailurePolicy::Drop);
async move { result }
}
}
#[cfg(test)]
mod tests {
use serde::Deserialize;
use super::*;
use crate::HeaderMap;
use crate::runtime::dispatch::Delivery;
#[derive(Debug, Deserialize)]
struct Meta {
task_id: u64,
}
#[derive(Default)]
struct Offset;
impl ContextField for Offset {
type Context = u64;
type Value = u64;
fn read(self, src: &u64) -> u64 {
*src
}
}
fn headers_with(task_id: &'static str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("task_id", task_id);
headers
}
#[test]
fn a_satisfied_header_contract_binds_the_typed_value() {
let state = ();
let delivery = Delivery::empty();
let headers = headers_with("7");
let mut ctx = Context::new("orders", &headers, &state, (), &delivery);
let Headers(meta) =
Headers::<Meta>::extract(&mut ctx, FailurePolicy::Drop).expect("contract holds");
assert_eq!(meta.task_id, 7);
}
#[test]
fn a_violated_contract_settles_by_the_configured_policy() {
let state = ();
let delivery = Delivery::empty();
let headers = headers_with("not a number");
let mut ctx = Context::new("orders", &headers, &state, (), &delivery);
assert!(
Headers::<Meta>::extract(&mut ctx, FailurePolicy::Drop)
.map(|_| ())
.unwrap_err()
.is_drop()
);
let mut ctx = Context::new("orders", &headers, &state, (), &delivery);
assert!(
Headers::<Meta>::extract(&mut ctx, FailurePolicy::Retry)
.map(|_| ())
.unwrap_err()
.is_retry()
);
let mut ctx = Context::new("orders", &headers, &state, (), &delivery);
assert!(
Headers::<Meta>::extract(&mut ctx, FailurePolicy::FailFast)
.map(|_| ())
.unwrap_err()
.is_drop()
);
}
#[tokio::test]
async fn the_plain_extractor_path_defaults_to_dropping_the_delivery() {
let state = ();
let delivery = Delivery::empty();
let headers = headers_with("not a number");
let mut ctx = Context::new("orders", &headers, &state, (), &delivery);
let outcome = <Headers<Meta> as FromContext<(), ()>>::from_context(&mut ctx).await;
assert!(outcome.map(|_| ()).unwrap_err().is_drop());
}
#[test]
fn a_context_field_parameter_shows_its_value_when_debugged() {
assert_eq!(format!("{:?}", Ctx::<Offset>(42)), "Ctx(42)");
}
}