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