webx_core/pipeline.rs
1//! Pipeline behavior trait: IPipelineBehavior.
2
3use crate::error::Result;
4use std::any::Any;
5use std::future::Future;
6use std::pin::Pin;
7
8/// Type-erased continuation function for pipeline behaviors.
9///
10/// Carries a boxed request to the next behavior (or terminal handler).
11pub type BoxedNextFn = Box<dyn FnOnce(Box<dyn Any + Send>) -> BoxedPipelineFuture + Send>;
12
13/// Boxed future returned by a pipeline behavior step.
14pub type BoxedPipelineFuture = Pin<Box<dyn Future<Output = Result<Box<dyn Any + Send>>> + Send>>;
15
16/// Pipeline behavior that wraps around request handling.
17///
18/// Multiple behaviors form a chain. Each behavior can:
19/// - Inspect or modify the request before passing it on
20/// - Inspect or modify the response after it returns
21/// - Short-circuit and skip the rest of the chain (by not calling `next`)
22///
23/// Analogous to MediatR's `IPipelineBehavior<TRequest, TResponse>`.
24///
25/// # Lifetime and dependencies
26///
27/// Behaviors are registered as **singletons**: one instance per host, shared by
28/// every request. The DI resolver is available only while the behavior is being
29/// **constructed** — use [`IServiceCollectionExt::add_pipeline_with`] and take
30/// your dependencies in the factory closure:
31///
32/// ```ignore
33/// svc.add_pipeline_with(|resolver| {
34/// // `&dyn IServiceResolver` is object-safe: resolve type-erased.
35/// let clock: Arc<Clock> = resolver
36/// .get_any(std::any::type_name::<Clock>())
37/// .and_then(|service| service.downcast::<Arc<Clock>>().ok())
38/// .map(|service| Arc::clone(&*service))
39/// .expect("register Clock before the pipeline");
40/// Arc::new(TimingBehavior { clock }) as Arc<dyn IPipelineBehavior>
41/// })
42/// ```
43///
44/// Inside [`IPipelineBehavior::handle`] there is no resolver and no typed
45/// request/response: the request and the eventual response are
46/// `Box<dyn Any + Send>`. Cross-cutting mechanics (timing, logging, cancellation)
47/// belong here; business rules that need the typed request belong in the handler
48/// or a use case it calls. [`IServiceCollectionExt::add_pipeline`] remains the
49/// zero-dependency convenience registration for `Default` behaviors.
50///
51/// # Response type contract
52///
53/// A behavior that short-circuits **must** return the same response type the
54/// handler would return (`downcast` to it happens after the chain, before the
55/// caller sees the value). Short-circuiting with a different type — for example
56/// `Box::new(())` — fails with an explicit type-mismatch error instead of
57/// silently succeeding.
58///
59/// [`IServiceCollectionExt::add_pipeline_with`]: crate::route::ext::IServiceCollectionExt::add_pipeline_with
60/// [`IServiceCollectionExt::add_pipeline`]: crate::route::ext::IServiceCollectionExt::add_pipeline
61#[async_trait::async_trait]
62pub trait IPipelineBehavior: Send + Sync {
63 async fn handle(
64 &self,
65 req: Box<dyn Any + Send>,
66 next: BoxedNextFn,
67 ) -> Result<Box<dyn Any + Send>>;
68}