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;