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` to `Request`            — enrich, validate, transform
6//! - `Request` to `Response`           — handler that produces a response
7//! - `Response` to `Response`          — post-process the outgoing message
8//! - `Request` to `Branch<Req, Resp>`  — 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
22/// Inbound message flowing through a pipeline.
23pub struct Request<T>(pub T);
24
25/// Outbound message produced by a pipeline. Wraps a `Result` so that
26/// downstream jigs can short-circuit on error.
27pub struct Response<T> {
28    /// The wrapped value, or an error message.
29    pub inner: Result<T, String>,
30}
31
32impl<T> Response<T> {
33    /// Construct a successful response.
34    pub fn ok(value: T) -> Self {
35        Self { inner: Ok(value) }
36    }
37    /// Construct an errored response from a message.
38    pub fn err(msg: impl Into<String>) -> Self {
39        Self {
40            inner: Err(msg.into()),
41        }
42    }
43    /// Returns `true` if this response carries a value.
44    pub fn is_ok(&self) -> bool {
45        self.inner.is_ok()
46    }
47    /// Returns `true` if this response carries an error.
48    pub fn is_err(&self) -> bool {
49        self.inner.is_err()
50    }
51}
52
53/// Outcome of a guard jig: either continue with a (possibly transformed)
54/// request, or short-circuit the pipeline with a response.
55pub enum Branch<Req, Resp> {
56    /// Continue the pipeline with this request.
57    Continue(Request<Req>),
58    /// Stop the pipeline and return this response.
59    Done(Response<Resp>),
60}
61
62/// One step in a jigs pipeline. Any `Fn(In) -> Out` automatically implements
63/// this trait, so plain functions, closures, and `#[jig]`-annotated functions
64/// can all be chained with `.then(...)`.
65pub trait Jig<In> {
66    /// The value produced by running this jig.
67    type Out;
68    /// Execute the jig on the given input.
69    fn run(&self, input: In) -> Self::Out;
70}
71
72impl<In, Out, F> Jig<In> for F
73where
74    F: Fn(In) -> Out,
75{
76    type Out = Out;
77    fn run(&self, input: In) -> Out {
78        (self)(input)
79    }
80}
81
82/// Wraps a future returned by an async jig so the chain remains spelled with `.then`.
83///
84/// The `#[jig]` macro converts `async fn` jigs into ordinary functions returning
85/// `Pending<impl Future<Output = T>>`. `Pending` itself impls `IntoFuture`, so the
86/// final `.await` resolves the whole chain.
87pub struct Pending<F>(pub F);
88
89/// Lifts the output of a jig into a future, so async and sync jigs can be chained
90/// uniformly inside a `Pending` chain. Sync values become a `Ready` future, a
91/// nested `Pending` is unwrapped to its inner future.
92pub trait Step {
93    /// Resolved output of this step.
94    type Out;
95    /// Future yielding the output.
96    type Fut: core::future::Future<Output = Self::Out>;
97    /// Convert this value into the future the chain awaits.
98    fn into_step(self) -> Self::Fut;
99}
100
101impl<T> Step for Request<T> {
102    type Out = Request<T>;
103    type Fut = core::future::Ready<Request<T>>;
104    fn into_step(self) -> Self::Fut {
105        core::future::ready(self)
106    }
107}
108
109impl<T> Step for Response<T> {
110    type Out = Response<T>;
111    type Fut = core::future::Ready<Response<T>>;
112    fn into_step(self) -> Self::Fut {
113        core::future::ready(self)
114    }
115}
116
117impl<R, P> Step for Branch<R, P> {
118    type Out = Branch<R, P>;
119    type Fut = core::future::Ready<Branch<R, P>>;
120    fn into_step(self) -> Self::Fut {
121        core::future::ready(self)
122    }
123}
124
125impl<F> Step for Pending<F>
126where
127    F: core::future::Future,
128{
129    type Out = F::Output;
130    type Fut = F;
131    fn into_step(self) -> Self::Fut {
132        self.0
133    }
134}
135
136impl<F> core::future::IntoFuture for Pending<F>
137where
138    F: core::future::Future,
139{
140    type Output = F::Output;
141    type IntoFuture = F;
142    fn into_future(self) -> F {
143        self.0
144    }
145}
146
147impl<F> Pending<F>
148where
149    F: core::future::Future + 'static,
150{
151    /// Append a jig to an in-flight async chain. The next jig may be sync
152    /// or async; sync values are lifted via [`Step`].
153    pub fn then<J, R>(self, jig: J) -> Pending<impl core::future::Future<Output = R::Out>>
154    where
155        J: Jig<F::Output, Out = R> + 'static,
156        R: Step + 'static,
157    {
158        Pending(async move {
159            let val = self.0.await;
160            jig.run(val).into_step().await
161        })
162    }
163}
164
165/// Common interface used by tracing to inspect a jig's outcome without
166/// knowing whether the value is a `Request`, `Response`, or `Branch`.
167pub trait Status {
168    /// Returns `true` if the value represents a successful outcome.
169    fn ok(&self) -> bool;
170    /// Error message, if any. Defaults to `None`.
171    fn error(&self) -> Option<String> {
172        None
173    }
174}
175
176impl<T> Status for Request<T> {
177    fn ok(&self) -> bool {
178        true
179    }
180}
181
182impl<T> Status for Response<T> {
183    fn ok(&self) -> bool {
184        self.is_ok()
185    }
186    fn error(&self) -> Option<String> {
187        self.inner.as_ref().err().cloned()
188    }
189}
190
191impl<Req, Resp> Status for Branch<Req, Resp> {
192    fn ok(&self) -> bool {
193        match self {
194            Branch::Continue(_) => true,
195            Branch::Done(r) => r.is_ok(),
196        }
197    }
198    fn error(&self) -> Option<String> {
199        match self {
200            Branch::Continue(_) => None,
201            Branch::Done(r) => r.inner.as_ref().err().cloned(),
202        }
203    }
204}
205
206/// Glue trait that lets a `Branch::then(jig)` accept a jig whose output is a
207/// `Request`, a `Response`, or another `Branch`, and merge the two outcomes
208/// into a single value.
209pub trait Merge<Resp> {
210    /// Result of merging this value with the prior `Branch`.
211    type Merged;
212    /// Called when the previous `Branch` was `Continue`.
213    fn into_continue(self) -> Self::Merged;
214    /// Called when the previous `Branch` was `Done`, propagating its response.
215    fn from_done(resp: Response<Resp>) -> Self::Merged;
216}
217
218impl<NewReq, Resp> Merge<Resp> for Request<NewReq> {
219    type Merged = Branch<NewReq, Resp>;
220    fn into_continue(self) -> Self::Merged {
221        Branch::Continue(self)
222    }
223    fn from_done(resp: Response<Resp>) -> Self::Merged {
224        Branch::Done(resp)
225    }
226}
227
228impl<Resp> Merge<Resp> for Response<Resp> {
229    type Merged = Response<Resp>;
230    fn into_continue(self) -> Self::Merged {
231        self
232    }
233    fn from_done(resp: Response<Resp>) -> Self::Merged {
234        resp
235    }
236}
237
238impl<NewReq, Resp> Merge<Resp> for Branch<NewReq, Resp> {
239    type Merged = Branch<NewReq, Resp>;
240    fn into_continue(self) -> Self::Merged {
241        self
242    }
243    fn from_done(resp: Response<Resp>) -> Self::Merged {
244        Branch::Done(resp)
245    }
246}
247
248impl<T> Request<T> {
249    /// Append the next jig to the pipeline.
250    pub fn then<J, U>(self, jig: J) -> U
251    where
252        J: Jig<Request<T>, Out = U>,
253    {
254        jig.run(self)
255    }
256}
257
258impl<T> Response<T> {
259    /// Append a `Response -> Response` jig. The jig always runs, including
260    /// on errored responses, so finalizers see every outcome. Jigs that
261    /// should only transform successful responses must check `is_ok` first.
262    pub fn then<J, U>(self, jig: J) -> Response<U>
263    where
264        J: Jig<Response<T>, Out = Response<U>>,
265    {
266        jig.run(self)
267    }
268}
269
270/// Multi-arm fork. Predicates are checked in order; the first match
271/// consumes the request and its jig is run. If none match, the
272/// `_ => default` arm runs. Every arm must produce the same `Out` type;
273/// each arm's internal pipeline can have its own intermediate types.
274///
275/// ```ignore
276/// fork!(req,
277///     |r| r.path.starts_with("/auth/")  => auth,
278///     |r| r.path.starts_with("/todos")  => todos,
279///     |r| r.path.starts_with("/labels") => labels,
280///     _ => not_found,
281/// )
282/// ```
283#[macro_export]
284macro_rules! fork {
285    ($req:expr, $($pred:expr => $jig:expr,)+ _ => $default:expr $(,)?) => {{
286        let __req = $req;
287        $crate::__fork_chain!(__req, $($pred => $jig,)+ ; $default)
288    }};
289}
290
291#[doc(hidden)]
292#[macro_export]
293macro_rules! __fork_chain {
294    ($req:ident, $pred:expr => $jig:expr, $($rest_p:expr => $rest_j:expr,)* ; $default:expr) => {
295        if ($pred)(&$req.0) {
296            ($jig)($req)
297        } else {
298            $crate::__fork_chain!($req, $($rest_p => $rest_j,)* ; $default)
299        }
300    };
301    ($req:ident, ; $default:expr) => {
302        ($default)($req)
303    };
304}
305
306impl<Req, Resp> Branch<Req, Resp> {
307    /// Append the next jig to a guarded pipeline. If the previous step was
308    /// `Done`, its response is propagated and `jig` is not run.
309    pub fn then<J>(self, jig: J) -> <J::Out as Merge<Resp>>::Merged
310    where
311        J: Jig<Request<Req>>,
312        J::Out: Merge<Resp>,
313    {
314        match self {
315            Branch::Continue(r) => <J::Out as Merge<Resp>>::into_continue(jig.run(r)),
316            Branch::Done(resp) => <J::Out as Merge<Resp>>::from_done(resp),
317        }
318    }
319}
320
321#[cfg(test)]
322mod tests;