Skip to main content

jigs_core/
lib.rs

1#![warn(missing_docs)]
2//! Core types for the `jigs` framework.
3//!
4//! A jig is one step in a request-to-response pipeline. Four kinds exist:
5//! - Request -> Request            — enrich, validate, transform
6//! - Request -> Response           — handler that produces a response
7//! - Response -> Response          — post-process the outgoing message
8//! - Request -> Branch<Request, Response>  — guard that may short-circuit
9//!
10//! Pipelines are built by chaining jigs with `.then(...)`. The type system
11//! enforces ordering: once you hold a [`Response`] you cannot chain a jig that
12//! expects a [`Request`]. `Branch::Done` and errored request-handling jigs
13//! short-circuit the request side of the pipeline, but once a [`Response`]
14//! exists every `Response -> Response` jig runs — including on errored
15//! responses — so finalizers (logging, headers, error envelopes) always
16//! see the outcome. Jigs that should only act on success must check
17//! [`Response::is_ok`] themselves.
18
19pub mod meta;
20pub use meta::{ChainKind, ChainStep, JigDef, JigMeta};
21
22pub mod json;
23
24#[doc(hidden)]
25pub trait __Classify {
26    const KIND: &'static str;
27}
28
29/// An inbound message flowing through a pipeline.
30///
31/// Types implementing this trait can be chained with `.then(jig)` on the
32/// request side.
33pub trait Request: Sized + __Classify {
34    /// Payload extracted from this request.
35    type Payload;
36    /// Borrow the payload.
37    fn payload(&self) -> &Self::Payload;
38    /// Consume the request and return the payload.
39    fn into_payload(self) -> Self::Payload;
40    /// Wrap a payload into a request.
41    fn from_payload(payload: Self::Payload) -> Self;
42
43    /// Append the next jig to the pipeline.
44    fn then<J, U>(self, jig: J) -> U
45    where
46        J: Jig<Self, Out = U>,
47    {
48        jig.run(self)
49    }
50}
51
52/// An outbound message produced by a pipeline.
53///
54/// Types implementing this trait wrap a `Result` so that downstream jigs can
55/// short-circuit on error.
56pub trait Response: Sized + __Classify {
57    /// The payload carried by a successful response.
58    type Payload;
59    /// Construct a successful response.
60    fn ok(payload: Self::Payload) -> Self;
61    /// Construct an errored response from a message.
62    fn err(msg: impl Into<String>) -> Self;
63    /// Returns `true` if this response carries a value.
64    fn is_ok(&self) -> bool;
65    /// Returns `true` if this response carries an error.
66    fn is_err(&self) -> bool {
67        !self.is_ok()
68    }
69    /// Convert into an owned `Result`.
70    ///
71    /// # Errors
72    /// Returns `Err` with the error message when the response carries an error.
73    fn into_result(self) -> Result<Self::Payload, String>;
74    /// Wrap a `Result` into a response.
75    fn from_result(result: Result<Self::Payload, String>) -> Self {
76        match result {
77            Ok(v) => Self::ok(v),
78            Err(e) => Self::err(e),
79        }
80    }
81    /// Non-consuming access to the error message, if this is an error.
82    fn error_msg(&self) -> Option<String> {
83        if self.is_err() {
84            Some("unknown error".into())
85        } else {
86            None
87        }
88    }
89
90    /// Append a `Response -> Response` jig. The jig always runs, including
91    /// on errored responses, so finalizers see every outcome.
92    fn then<J>(self, jig: J) -> J::Out
93    where
94        J: Jig<Self>,
95        J::Out: Response,
96    {
97        jig.run(self)
98    }
99}
100
101/// Outcome of a guard jig: either continue with a (possibly transformed)
102/// request, or short-circuit the pipeline with a response.
103#[derive(Debug)]
104pub enum Branch<Req, Resp> {
105    /// Continue the pipeline with this request.
106    Continue(Req),
107    /// Stop the pipeline and return this response.
108    Done(Resp),
109}
110
111impl<Req: Request, Resp: Response> __Classify for Branch<Req, Resp> {
112    const KIND: &'static str = "Branch";
113}
114
115impl<Req, Resp> Branch<Req, Resp> {
116    /// Returns `true` if this is `Branch::Continue`.
117    #[must_use]
118    pub fn is_continue(&self) -> bool {
119        matches!(self, Branch::Continue(_))
120    }
121
122    /// Returns `true` if this is `Branch::Done`.
123    #[must_use]
124    pub fn is_done(&self) -> bool {
125        matches!(self, Branch::Done(_))
126    }
127}
128
129/// One step in a jigs pipeline. Any `Fn(In) -> Out` automatically implements
130/// this trait, so plain functions, closures, and `#[jig]`-annotated functions
131/// can all be chained with `.then(...)`.
132pub trait Jig<In> {
133    /// The value produced by running this jig.
134    type Out;
135    /// Execute the jig on the given input.
136    fn run(&self, input: In) -> Self::Out;
137}
138
139impl<In, Out, F> Jig<In> for F
140where
141    F: Fn(In) -> Out,
142{
143    type Out = Out;
144    fn run(&self, input: In) -> Out {
145        (self)(input)
146    }
147}
148
149/// Wraps a future returned by an async jig so the chain remains spelled with `.then`.
150///
151/// The `#[jig]` macro converts `async fn` jigs into ordinary functions returning
152/// `Pending<impl Future<Output = T>>`. `Pending` itself impls `IntoFuture`, so the
153/// final `.await` resolves the whole chain.
154pub struct Pending<F>(pub F);
155
156impl<F> __Classify for Pending<F> {
157    const KIND: &'static str = "Pending";
158}
159
160/// Lifts the output of a jig into a future, so async and sync jigs can be chained
161/// uniformly inside a `Pending` chain. Sync values become a `Ready` future, a
162/// nested `Pending` is unwrapped to its inner future.
163pub trait Step {
164    /// Resolved output of this step.
165    type Out;
166    /// Future yielding the output.
167    type Fut: core::future::Future<Output = Self::Out>;
168    /// Convert this value into the future the chain awaits.
169    fn into_step(self) -> Self::Fut;
170}
171
172impl<REQ, RESP> Step for Branch<REQ, RESP>
173where
174    REQ: Request,
175    RESP: Response,
176{
177    type Out = Branch<REQ, RESP>;
178    type Fut = core::future::Ready<Branch<REQ, RESP>>;
179    fn into_step(self) -> Self::Fut {
180        core::future::ready(self)
181    }
182}
183
184impl<F> Step for Pending<F>
185where
186    F: core::future::Future,
187{
188    type Out = F::Output;
189    type Fut = F;
190    fn into_step(self) -> Self::Fut {
191        self.0
192    }
193}
194
195impl<F> core::future::IntoFuture for Pending<F>
196where
197    F: core::future::Future,
198{
199    type Output = F::Output;
200    type IntoFuture = F;
201    fn into_future(self) -> F {
202        self.0
203    }
204}
205
206impl<F> Pending<F>
207where
208    F: core::future::Future + 'static,
209{
210    /// Append a jig to an in-flight async chain. The next jig may be sync
211    /// or async; sync values are lifted via [`Step`].
212    pub fn then<J, R>(self, jig: J) -> Pending<impl core::future::Future<Output = R::Out>>
213    where
214        J: Jig<F::Output, Out = R> + 'static,
215        R: Step + 'static,
216    {
217        Pending(async move {
218            let val = self.0.await;
219            jig.run(val).into_step().await
220        })
221    }
222}
223
224/// Common interface used by tracing to inspect a jig's outcome without
225/// knowing whether the value is a `Request`, `Response`, or `Branch`.
226pub trait Status {
227    /// Returns `true` if the value represents a successful outcome.
228    fn succeeded(&self) -> bool;
229    /// Error message, if any. Defaults to `None`.
230    fn error(&self) -> Option<String> {
231        None
232    }
233}
234
235impl<REQ, RESP> Status for Branch<REQ, RESP>
236where
237    REQ: Request,
238    RESP: Response,
239{
240    fn succeeded(&self) -> bool {
241        match self {
242            Branch::Continue(_) => true,
243            Branch::Done(r) => r.is_ok(),
244        }
245    }
246    fn error(&self) -> Option<String> {
247        match self {
248            Branch::Continue(_) => None,
249            Branch::Done(r) => r.error_msg(),
250        }
251    }
252}
253
254/// Glue trait that lets a `Branch::then(jig)` accept a jig whose output is a
255/// request, a response, or another `Branch`, and merge the two outcomes
256/// into a single value.
257///
258/// Implement this for custom request/response types when you need to use them
259/// after a `Branch`. See the [`impl_request!`] and [`impl_response!`]
260/// convenience macros, or derive [`Request`] / [`Response`] which generate this
261/// automatically.
262pub trait Merge<R> {
263    /// Result of merging this value with the prior `Branch`.
264    type Merged;
265    /// Called when the previous `Branch` was `Continue`.
266    fn into_continue(self) -> Self::Merged;
267    /// Called when the previous `Branch` was `Done`, propagating its response.
268    fn from_done(resp: R) -> Self::Merged;
269}
270
271impl<REQ, RESP> Merge<RESP> for Branch<REQ, RESP>
272where
273    REQ: Request,
274    RESP: Response,
275{
276    type Merged = Branch<REQ, RESP>;
277    fn into_continue(self) -> Self::Merged {
278        self
279    }
280    fn from_done(resp: RESP) -> Self::Merged {
281        Branch::Done(resp)
282    }
283}
284
285impl<REQ, RESP> Branch<REQ, RESP>
286where
287    REQ: Request,
288    RESP: Response,
289{
290    /// Append the next jig to a guarded pipeline. If the previous step was
291    /// `Done`, its response is propagated and `jig` is not run.
292    #[allow(clippy::needless_pass_by_value)]
293    pub fn then<J, Out>(self, jig: J) -> <Out as Merge<RESP>>::Merged
294    where
295        J: Jig<REQ, Out = Out>,
296        Out: Merge<RESP>,
297    {
298        match self {
299            Branch::Continue(r) => Out::into_continue(jig.run(r)),
300            Branch::Done(resp) => Out::from_done(resp),
301        }
302    }
303}
304
305/// Wire a custom request type into the framework.
306///
307/// Generates `Merge<R>`, `Status`, and `Step` so the type works in standard
308/// pipelines and async chains.
309///
310/// ```ignore
311/// impl_request!(MyReq);
312/// ```
313#[macro_export]
314macro_rules! impl_request {
315    ($t:ty) => {
316        impl $crate::__Classify for $t {
317            const KIND: &'static str = "Request";
318        }
319        impl $crate::Step for $t {
320            type Out = $t;
321            type Fut = ::core::future::Ready<$t>;
322            fn into_step(self) -> Self::Fut {
323                ::core::future::ready(self)
324            }
325        }
326        impl<R: $crate::Response> $crate::Merge<R> for $t {
327            type Merged = $crate::Branch<$t, R>;
328            fn into_continue(self) -> Self::Merged {
329                $crate::Branch::Continue(self)
330            }
331            fn from_done(resp: R) -> Self::Merged {
332                $crate::Branch::Done(resp)
333            }
334        }
335        impl $crate::Status for $t {
336            fn succeeded(&self) -> bool {
337                true
338            }
339            fn error(&self) -> Option<String> {
340                None
341            }
342        }
343    };
344}
345
346/// Wire a custom response type into the framework.
347///
348/// Generates `Merge<Self>`, `Status`, and `Step` so the type works in standard
349/// pipelines and async chains.
350///
351/// ```ignore
352/// impl_response!(MyResp);
353/// ```
354#[macro_export]
355macro_rules! impl_response {
356    ($t:ty) => {
357        impl $crate::__Classify for $t {
358            const KIND: &'static str = "Response";
359        }
360        impl $crate::Step for $t {
361            type Out = $t;
362            type Fut = ::core::future::Ready<$t>;
363            fn into_step(self) -> Self::Fut {
364                ::core::future::ready(self)
365            }
366        }
367        impl $crate::Merge<$t> for $t {
368            type Merged = $t;
369            fn into_continue(self) -> Self::Merged {
370                self
371            }
372            fn from_done(resp: $t) -> Self::Merged {
373                resp
374            }
375        }
376        impl $crate::Status for $t {
377            fn succeeded(&self) -> bool {
378                $crate::Response::is_ok(self)
379            }
380            fn error(&self) -> Option<String> {
381                $crate::Response::error_msg(self)
382            }
383        }
384    };
385}
386
387/// Multi-arm fork. Predicates are checked in order; the first match
388/// consumes the request and its jig is run. If none match, the
389/// `_ => default` arm runs. Every arm must produce the same `Out` type;
390/// each arm's internal pipeline can have its own intermediate types.
391///
392/// ```ignore
393/// fork!(req,
394///     |r| r.path.starts_with("/auth/")  => auth,
395///     |r| r.path.starts_with("/todos")  => todos,
396///     |r| r.path.starts_with("/labels") => labels,
397///     _ => not_found,
398/// )
399/// ```
400#[macro_export]
401macro_rules! fork {
402    ($req:expr, $($pred:expr => $jig:expr,)+ _ => $default:expr $(,)?) => {{
403        let __req = $req;
404        $crate::__fork_chain!(__req, $($pred => $jig,)+ ; $default)
405    }};
406}
407
408#[doc(hidden)]
409#[macro_export]
410macro_rules! __fork_chain {
411    ($req:ident, $pred:expr => $jig:expr, $($rest_p:expr => $rest_j:expr,)* ; $default:expr) => {
412        if ($pred)($crate::Request::payload(&$req)) {
413            ($jig)($req)
414        } else {
415            $crate::__fork_chain!($req, $($rest_p => $rest_j,)* ; $default)
416        }
417    };
418    ($req:ident, ; $default:expr) => {
419        ($default)($req)
420    };
421}
422
423#[cfg(test)]
424mod tests;