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