webx_core/pipeline.rs
1//! Pipeline behaviors: typed (`IPipelineBehavior<Req, Rsp>`) and the legacy
2//! type-erased variant (`IErasedPipelineBehavior`).
3
4use crate::error::Result;
5use std::any::Any;
6use std::future::Future;
7use std::pin::Pin;
8
9/// Type-erased continuation function used by [`IErasedPipelineBehavior`].
10pub type BoxedNextFn = Box<dyn FnOnce(Box<dyn Any + Send>) -> BoxedPipelineFuture + Send>;
11
12/// Boxed future returned by a type-erased pipeline step.
13pub type BoxedPipelineFuture = Pin<Box<dyn Future<Output = Result<Box<dyn Any + Send>>> + Send>>;
14
15/// The continuation a typed behavior calls to run the rest of the chain.
16///
17/// The framework builds this value; behaviors only consume it:
18///
19/// ```ignore
20/// let response = next.run(req).await?; // run the remaining chain + handler
21/// ```
22/// Boxed future returned by one typed pipeline step.
23type NextFuture<Rsp> = Pin<Box<dyn Future<Output = Result<Rsp>> + Send>>;
24
25/// Callable continuation stored by [`Next`].
26type NextCall<Req, Rsp> = Box<dyn FnOnce(Req) -> NextFuture<Rsp> + Send>;
27
28pub struct Next<Req, Rsp>
29where
30 Req: Send + 'static,
31 Rsp: Send + 'static,
32{
33 call: NextCall<Req, Rsp>,
34}
35
36impl<Req, Rsp> Next<Req, Rsp>
37where
38 Req: Send + 'static,
39 Rsp: Send + 'static,
40{
41 /// Wrap a typed continuation.
42 pub fn new<F, Fut>(call: F) -> Self
43 where
44 F: FnOnce(Req) -> Fut + Send + 'static,
45 Fut: Future<Output = Result<Rsp>> + Send + 'static,
46 {
47 Self {
48 call: Box::new(move |req| Box::pin(call(req))),
49 }
50 }
51
52 /// Run the rest of the chain (and the handler) with `req`.
53 pub async fn run(self, req: Req) -> Result<Rsp> {
54 (self.call)(req).await
55 }
56}
57
58/// Typed pipeline behavior that wraps around request handling.
59///
60/// Multiple behaviors form a chain: the first registered behavior is the
61/// outermost (runs first on the request, last on the response). A behavior can
62/// inspect/modify the request and response, or **short-circuit** by returning a
63/// response without calling [`Next::run`] — the value is the handler's response
64/// type, checked by the compiler:
65///
66/// ```ignore
67/// #[derive(Default)]
68/// struct RequireNotes;
69///
70/// #[async_trait::async_trait]
71/// impl IPipelineBehavior<CreateOrderRequest, OrderView> for RequireNotes {
72/// async fn handle(
73/// &self,
74/// req: CreateOrderRequest,
75/// next: Next<CreateOrderRequest, OrderView>,
76/// ) -> Result<OrderView> {
77/// if req.note.is_empty() {
78/// return Err(Error::Validation("note is required".into()));
79/// }
80/// next.run(req).await
81/// }
82/// }
83///
84/// svc.add_typed_pipeline::<RequireNotes, CreateOrderRequest, OrderView>();
85/// ```
86///
87/// Behaviors are **singletons** (one instance per host). The DI resolver is only
88/// available while a behavior is being **constructed**:
89/// [`IServiceCollectionExt::add_typed_pipeline_with`] hands the factory the
90/// resolver; inside `handle` only the typed request/continuation exist.
91///
92/// [`IServiceCollectionExt::add_typed_pipeline_with`]:
93/// crate::route::ext::IServiceCollectionExt::add_typed_pipeline_with
94#[async_trait::async_trait]
95pub trait IPipelineBehavior<Req: Send + 'static, Rsp: Send + 'static>: Send + Sync {
96 async fn handle(&self, req: Req, next: Next<Req, Rsp>) -> Result<Rsp>;
97}
98
99/// Legacy type-erased pipeline behavior (deprecated since 0.10.0).
100///
101/// Kept for one release so existing behaviors keep compiling and running: the
102/// erased chain runs **outside** the typed chain, so it observes the request
103/// before typed behaviors and the response after them. A short-circuit here
104/// must still return the handler's response type as `Box<dyn Any + Send>`;
105/// prefer [`IPipelineBehavior`] for new code.
106#[deprecated(
107 since = "0.10.0",
108 note = "use the typed `IPipelineBehavior<Req, Rsp>`; the erased trait is removed in 1.0"
109)]
110#[async_trait::async_trait]
111pub trait IErasedPipelineBehavior: Send + Sync {
112 async fn handle(
113 &self,
114 req: Box<dyn Any + Send>,
115 next: BoxedNextFn,
116 ) -> Result<Box<dyn Any + Send>>;
117}