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