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