Skip to main content

pacta_executor/
lib.rs

1//! Pacta-native execution abstractions.
2
3#![forbid(unsafe_code)]
4#![warn(missing_docs)]
5
6use pacta_contract::Pact;
7
8pub use pacta_contract::{Outcome, Settlement};
9
10/// A single attempt to fulfill a claimed pact.
11///
12/// This is the executor's designated input seam. It is `#[non_exhaustive]` so it can
13/// gain execution-context fields in a later minor release without a breaking change;
14/// construct it through [`Execution::new`].
15#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub struct Execution {
18    /// Pact being executed.
19    pub pact: Pact,
20}
21
22impl Execution {
23    /// Build an execution from a claimed pact.
24    #[must_use]
25    pub fn new(pact: Pact) -> Self {
26        Self { pact }
27    }
28}
29
30/// Public role responsible for executing claimed pacts through middleware.
31pub trait Executor {
32    /// Error returned when the execution infrastructure fails.
33    type Error: std::error::Error;
34
35    /// Execute a claimed pact.
36    fn execute(&mut self, execution: Execution) -> Result<Outcome, Self::Error>;
37}
38
39/// The decision a [`Policy`] renders for one infrastructure failure.
40///
41/// This is a closed, two-variant decision (deliberately not `#[non_exhaustive]`, unlike the
42/// growing kernel protocol enums): it is the complete answer to one question — keep lapsing,
43/// or concede — not a protocol that accretes new cases over time.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Verdict {
46    /// Propagate the error so the claim is left unsettled: it lapses and may be reclaimed
47    /// and attempted again by a later step.
48    Continue,
49    /// Give up: settle the claim as breached now instead of letting it lapse again.
50    Concede,
51}
52
53/// A user-obligation trait deciding, after a claimed pact's execution fails with an
54/// infrastructure error, whether to keep letting the claim lapse (to be reclaimed and
55/// attempted again) or concede as a terminal breach.
56///
57/// This governs only infrastructure failures — an [`Executor::Error`] the
58/// shipped `Driver` would otherwise leave unsettled to lapse and be reclaimed indefinitely.
59/// It has no bearing on a clean business [`Outcome`]: the shipped `Driver` always settles a
60/// clean `Outcome::Breached` as terminal, so there is no "retry a business breach" decision
61/// for a `Policy` to make (see `BACKLOG.md`'s `lifecycle-persistence` entry: attempt limits
62/// stay outside the registry, as user-owned policy — this trait is that policy's seam).
63pub trait Policy<E> {
64    /// Decide the verdict for the `attempts`-th consecutive infrastructure failure (counting
65    /// the current one) with `error`.
66    fn decide(&self, attempts: u32, error: &E) -> Verdict;
67}
68
69/// A Pacta-native decorator over execution: the Tower `Layer` analog. Because `wrap`
70/// takes an `Executor` and returns an `Executor`, middleware compose arbitrarily
71/// (the closure property), which is how orchestration is composed onto the seam.
72pub trait Middleware<E> {
73    /// The wrapped executor type.
74    type Executor: Executor;
75
76    /// Wrap an executor with this middleware.
77    fn wrap(&self, executor: E) -> Self::Executor;
78}
79
80/// The no-op middleware: `wrap` returns the executor unchanged. `Identity` is the
81/// neutral element of composition — the empty stack — so "zero middleware" is a
82/// first-class, holdable value rather than an absence.
83#[derive(Debug, Default, Clone, Copy)]
84pub struct Identity;
85
86impl<E: Executor> Middleware<E> for Identity {
87    type Executor = E;
88
89    fn wrap(&self, executor: E) -> Self::Executor {
90        executor
91    }
92}
93
94/// Two middleware composed into one, reifying the closure property as a value: because
95/// `Stack` is itself a [`Middleware`], a composed stack can be named, stored, and passed
96/// as one middleware *before* an executor exists. `outer` wraps the result of `inner`,
97/// so `outer` is applied last and therefore observes each execution first.
98#[derive(Debug, Default, Clone, Copy)]
99pub struct Stack<Inner, Outer> {
100    inner: Inner,
101    outer: Outer,
102}
103
104impl<Inner, Outer> Stack<Inner, Outer> {
105    /// Compose `inner` and `outer` into one middleware. `outer` wraps `inner`'s result,
106    /// so `outer` observes each execution first.
107    #[must_use]
108    pub const fn new(inner: Inner, outer: Outer) -> Self {
109        Self { inner, outer }
110    }
111}
112
113impl<E, Inner, Outer> Middleware<E> for Stack<Inner, Outer>
114where
115    E: Executor,
116    Inner: Middleware<E>,
117    Outer: Middleware<Inner::Executor>,
118{
119    type Executor = Outer::Executor;
120
121    fn wrap(&self, executor: E) -> Self::Executor {
122        self.outer.wrap(self.inner.wrap(executor))
123    }
124}
125
126/// A blind, ordered assembly of middleware composed over [`Identity`]. It is itself a
127/// [`Middleware`], so applying it is just `wrap`.
128///
129/// It is deliberately *blind*: [`Composition::then`] accepts any `Middleware` through a
130/// single generic operation and inspects nothing, and the type offers no method named for
131/// an orchestration policy (retry, timeout, backoff, circuit, quota, rate-limit) — that
132/// policy is a consumer or sibling concern, never a core convenience.
133///
134/// # Order
135///
136/// Middleware are applied outermost-first in the order added: the **first** middleware
137/// added with [`then`](Composition::then) is the outermost and observes each execution
138/// **first**; the executor is innermost. This mirrors the assembly convention of the
139/// prior art the mechanism is distilled from.
140#[derive(Debug, Default, Clone, Copy)]
141pub struct Composition<M> {
142    middleware: M,
143}
144
145impl Composition<Identity> {
146    /// Start an empty composition over [`Identity`].
147    #[must_use]
148    pub const fn new() -> Self {
149        Self {
150            middleware: Identity,
151        }
152    }
153}
154
155impl<M> Composition<M> {
156    /// Add a middleware to the composition. Blind: it accepts any `Middleware` and
157    /// inspects nothing. The first middleware added is outermost (observes each
158    /// execution first); the executor is innermost.
159    #[must_use]
160    pub fn then<N>(self, next: N) -> Composition<Stack<N, M>> {
161        Composition {
162            middleware: Stack::new(next, self.middleware),
163        }
164    }
165}
166
167impl<E, M> Middleware<E> for Composition<M>
168where
169    E: Executor,
170    M: Middleware<E>,
171{
172    type Executor = M::Executor;
173
174    fn wrap(&self, executor: E) -> Self::Executor {
175        self.middleware.wrap(executor)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use std::collections::HashMap;
182
183    use pacta_contract::Uuid;
184
185    use super::*;
186
187    #[derive(Debug)]
188    struct DummyError;
189
190    impl std::fmt::Display for DummyError {
191        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192            write!(f, "dummy error")
193        }
194    }
195
196    impl std::error::Error for DummyError {}
197
198    struct DummyExecutor;
199    impl Executor for DummyExecutor {
200        type Error = DummyError;
201        fn execute(&mut self, _execution: Execution) -> Result<Outcome, Self::Error> {
202            Ok(Outcome::Fulfilled)
203        }
204    }
205
206    struct IdentityExecutor<E> {
207        inner: E,
208    }
209    impl<E: Executor> Executor for IdentityExecutor<E> {
210        type Error = E::Error;
211        fn execute(&mut self, execution: Execution) -> Result<Outcome, Self::Error> {
212            self.inner.execute(execution)
213        }
214    }
215
216    struct IdentityMiddleware;
217    impl<E: Executor> Middleware<E> for IdentityMiddleware {
218        type Executor = IdentityExecutor<E>;
219        fn wrap(&self, executor: E) -> Self::Executor {
220            IdentityExecutor { inner: executor }
221        }
222    }
223
224    struct BreachExecutor<E> {
225        _inner: E,
226    }
227    impl<E: Executor> Executor for BreachExecutor<E> {
228        type Error = E::Error;
229        fn execute(&mut self, _execution: Execution) -> Result<Outcome, Self::Error> {
230            Ok(Outcome::Breached)
231        }
232    }
233
234    struct BreachMiddleware;
235    impl<E: Executor> Middleware<E> for BreachMiddleware {
236        type Executor = BreachExecutor<E>;
237        fn wrap(&self, executor: E) -> Self::Executor {
238            BreachExecutor { _inner: executor }
239        }
240    }
241
242    fn dummy_execution() -> Execution {
243        Execution::new(Pact::new(
244            Default::default(),
245            "dummy_docket".to_string(),
246            "dummy_kind".to_string(),
247            vec![],
248        ))
249    }
250
251    #[test]
252    fn identity_middleware_preserves_fulfilled() {
253        let middleware = IdentityMiddleware;
254        let mut executor = middleware.wrap(DummyExecutor);
255        let outcome = executor.execute(dummy_execution()).unwrap();
256        assert_eq!(outcome, Outcome::Fulfilled);
257    }
258
259    #[test]
260    fn breach_middleware_alters_outcome() {
261        let middleware = BreachMiddleware;
262        let mut executor = middleware.wrap(DummyExecutor);
263        let outcome = executor.execute(dummy_execution()).unwrap();
264        assert_eq!(outcome, Outcome::Breached);
265    }
266
267    #[test]
268    fn stacked_middleware_composes() {
269        // The closure property: `Middleware` wraps `Executor` into `Executor`, so
270        // two middleware stack and still yield a working executor. This proves the
271        // "compose the rest" seam holds beyond a single wrap, and guards the
272        // `Middleware<E>` generic shape from regressing so it can no longer stack.
273        let inner = IdentityMiddleware.wrap(DummyExecutor);
274        let mut stacked = IdentityMiddleware.wrap(inner);
275        let outcome = stacked.execute(dummy_execution()).unwrap();
276        assert_eq!(outcome, Outcome::Fulfilled);
277    }
278
279    #[test]
280    fn stacked_middleware_preserves_ordering() {
281        // A breach layer wrapping an identity layer over a fulfilling executor still
282        // composes to a working executor whose outcome is the outermost layer's.
283        let inner = IdentityMiddleware.wrap(DummyExecutor);
284        let mut stacked = BreachMiddleware.wrap(inner);
285        let outcome = stacked.execute(dummy_execution()).unwrap();
286        assert_eq!(outcome, Outcome::Breached);
287    }
288
289    #[test]
290    fn identity_wrap_returns_the_executor_unchanged() {
291        // Identity is the neutral element: wrapping adds nothing.
292        let mut executor = Identity.wrap(DummyExecutor);
293        assert_eq!(
294            executor.execute(dummy_execution()).unwrap(),
295            Outcome::Fulfilled
296        );
297    }
298
299    #[test]
300    fn stack_is_itself_a_middleware() {
301        // Stack reifies the closure property as a value: a composed pair is one Middleware
302        // that wraps an executor into an executor just like a single middleware does.
303        let stack = Stack::new(IdentityMiddleware, BreachMiddleware);
304        let mut executor = stack.wrap(DummyExecutor);
305        // BreachMiddleware is `outer`, so it is applied last and observes execution first.
306        assert_eq!(
307            executor.execute(dummy_execution()).unwrap(),
308            Outcome::Breached
309        );
310    }
311
312    #[test]
313    fn composition_assembles_and_drives_to_a_settlement() {
314        // The blind assembler composes two pass-through middleware over Identity and
315        // drives to a settlement — the reified mechanism proven to compose.
316        let composed = Composition::new()
317            .then(IdentityMiddleware)
318            .then(IdentityMiddleware);
319        let mut executor = composed.wrap(DummyExecutor);
320        assert_eq!(
321            executor.execute(dummy_execution()).unwrap(),
322            Outcome::Fulfilled
323        );
324    }
325
326    #[test]
327    fn composition_orders_first_added_outermost() {
328        use std::cell::RefCell;
329        use std::rc::Rc;
330
331        // A middleware that records both when its layer is *entered* (before delegating inward) and
332        // *exited* (after the inner execution returns), so the full nesting is observable — not just
333        // the final outcome. A regression that inverted the nesting but kept the outcome would still
334        // be caught by comparing the whole trace.
335        struct RecordingExecutor<E> {
336            inner: E,
337            label: &'static str,
338            log: Rc<RefCell<Vec<String>>>,
339        }
340        impl<E: Executor> Executor for RecordingExecutor<E> {
341            type Error = E::Error;
342            fn execute(&mut self, execution: Execution) -> Result<Outcome, Self::Error> {
343                self.log.borrow_mut().push(format!("{}:enter", self.label));
344                let outcome = self.inner.execute(execution);
345                self.log.borrow_mut().push(format!("{}:exit", self.label));
346                outcome
347            }
348        }
349        struct Recorder {
350            label: &'static str,
351            log: Rc<RefCell<Vec<String>>>,
352        }
353        impl<E: Executor> Middleware<E> for Recorder {
354            type Executor = RecordingExecutor<E>;
355            fn wrap(&self, inner: E) -> Self::Executor {
356                RecordingExecutor {
357                    inner,
358                    label: self.label,
359                    log: Rc::clone(&self.log),
360                }
361            }
362        }
363
364        // The innermost executor records that it ran, so the trace shows the executor is innermost.
365        struct RecordingInner {
366            log: Rc<RefCell<Vec<String>>>,
367        }
368        impl Executor for RecordingInner {
369            type Error = DummyError;
370            fn execute(&mut self, _execution: Execution) -> Result<Outcome, Self::Error> {
371                self.log.borrow_mut().push("executor".to_string());
372                Ok(Outcome::Fulfilled)
373            }
374        }
375
376        let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
377        let composed = Composition::new()
378            .then(Recorder {
379                label: "first",
380                log: Rc::clone(&log),
381            })
382            .then(Recorder {
383                label: "second",
384                log: Rc::clone(&log),
385            });
386        let mut executor = composed.wrap(RecordingInner {
387            log: Rc::clone(&log),
388        });
389        executor.execute(dummy_execution()).unwrap();
390
391        // Full trace: the first-added middleware is outermost — entered first, exited last; the
392        // second is nested within it; the executor is innermost.
393        assert_eq!(
394            *log.borrow(),
395            vec![
396                "first:enter",
397                "second:enter",
398                "executor",
399                "second:exit",
400                "first:exit",
401            ]
402        );
403    }
404
405    // In-workspace reference validator for `Policy`, per `composition-governance`'s "trait's
406    // in-workspace validator may be test-only scaffolding" scenario: never public API.
407
408    #[derive(Clone, Copy)]
409    struct FixedThreshold {
410        threshold: u32,
411    }
412
413    impl Policy<DummyError> for FixedThreshold {
414        fn decide(&self, attempts: u32, _error: &DummyError) -> Verdict {
415            if attempts >= self.threshold {
416                Verdict::Concede
417            } else {
418                Verdict::Continue
419            }
420        }
421    }
422
423    struct FailingExecutor;
424    impl Executor for FailingExecutor {
425        type Error = DummyError;
426        fn execute(&mut self, _execution: Execution) -> Result<Outcome, Self::Error> {
427            Err(DummyError)
428        }
429    }
430
431    struct GiveUpExecutor<Inner, P> {
432        inner: Inner,
433        policy: P,
434        attempts: HashMap<Uuid, u32>,
435    }
436
437    impl<Inner, P> Executor for GiveUpExecutor<Inner, P>
438    where
439        Inner: Executor,
440        P: Policy<Inner::Error>,
441    {
442        type Error = Inner::Error;
443
444        fn execute(&mut self, execution: Execution) -> Result<Outcome, Self::Error> {
445            let id = execution.pact.id;
446            match self.inner.execute(execution) {
447                Ok(outcome) => {
448                    self.attempts.remove(&id);
449                    Ok(outcome)
450                }
451                Err(error) => {
452                    let attempts = self.attempts.entry(id).or_insert(0);
453                    *attempts += 1;
454                    match self.policy.decide(*attempts, &error) {
455                        Verdict::Continue => Err(error),
456                        Verdict::Concede => Ok(Outcome::Breached),
457                    }
458                }
459            }
460        }
461    }
462
463    struct GiveUp<P> {
464        policy: P,
465    }
466
467    impl<E, P> Middleware<E> for GiveUp<P>
468    where
469        E: Executor,
470        P: Policy<E::Error> + Clone,
471    {
472        type Executor = GiveUpExecutor<E, P>;
473
474        fn wrap(&self, executor: E) -> Self::Executor {
475            GiveUpExecutor {
476                inner: executor,
477                policy: self.policy.clone(),
478                attempts: HashMap::new(),
479            }
480        }
481    }
482
483    fn execution_with_id(id: Uuid) -> Execution {
484        Execution::new(Pact::new(
485            id,
486            "dummy_docket".to_string(),
487            "dummy_kind".to_string(),
488            vec![],
489        ))
490    }
491
492    #[test]
493    fn below_threshold_keeps_propagating_the_error() {
494        let mut executor = GiveUp {
495            policy: FixedThreshold { threshold: 3 },
496        }
497        .wrap(FailingExecutor);
498        let id = Uuid::from_u128(1);
499
500        assert!(executor.execute(execution_with_id(id)).is_err());
501        assert!(executor.execute(execution_with_id(id)).is_err());
502    }
503
504    #[test]
505    fn reaching_threshold_concedes_as_breached() {
506        let mut executor = GiveUp {
507            policy: FixedThreshold { threshold: 3 },
508        }
509        .wrap(FailingExecutor);
510        let id = Uuid::from_u128(2);
511
512        assert!(executor.execute(execution_with_id(id)).is_err());
513        assert!(executor.execute(execution_with_id(id)).is_err());
514        assert_eq!(
515            executor.execute(execution_with_id(id)).unwrap(),
516            Outcome::Breached
517        );
518    }
519
520    #[test]
521    fn a_success_resets_the_failure_count() {
522        struct FlakyThenFulfilling {
523            fail_next: bool,
524        }
525        impl Executor for FlakyThenFulfilling {
526            type Error = DummyError;
527            fn execute(&mut self, _execution: Execution) -> Result<Outcome, Self::Error> {
528                if self.fail_next {
529                    Err(DummyError)
530                } else {
531                    Ok(Outcome::Fulfilled)
532                }
533            }
534        }
535
536        let mut executor = GiveUp {
537            policy: FixedThreshold { threshold: 2 },
538        }
539        .wrap(FlakyThenFulfilling { fail_next: true });
540        let id = Uuid::from_u128(3);
541
542        // attempts = 1, below the threshold of 2.
543        assert!(executor.execute(execution_with_id(id)).is_err());
544
545        // A success in between resets the count instead of letting it carry toward the
546        // threshold.
547        executor.inner.fail_next = false;
548        assert_eq!(
549            executor.execute(execution_with_id(id)).unwrap(),
550            Outcome::Fulfilled
551        );
552
553        // Failing again starts back at attempts = 1, still below the threshold of 2.
554        executor.inner.fail_next = true;
555        assert!(executor.execute(execution_with_id(id)).is_err());
556    }
557
558    #[test]
559    fn distinct_pacts_are_tracked_independently() {
560        let mut executor = GiveUp {
561            policy: FixedThreshold { threshold: 2 },
562        }
563        .wrap(FailingExecutor);
564        let first = Uuid::from_u128(4);
565        let second = Uuid::from_u128(5);
566
567        assert!(executor.execute(execution_with_id(first)).is_err());
568        assert!(executor.execute(execution_with_id(second)).is_err());
569
570        // Each pact's own count reaches the threshold independently, not a shared one.
571        assert_eq!(
572            executor.execute(execution_with_id(first)).unwrap(),
573            Outcome::Breached
574        );
575        assert_eq!(
576            executor.execute(execution_with_id(second)).unwrap(),
577            Outcome::Breached
578        );
579    }
580}