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