pacta_contract/lib.rs
1//! The isolated core contract for Pacta.
2//!
3//! Pacta operates on three axioms:
4//! 1. Registry is Lifecycle (no business logic, no retry/delay logic).
5//! 2. Execution is Middleware.
6//! 3. This contract has no dependency on other workspace crates.
7
8#![forbid(unsafe_code)]
9#![warn(missing_docs)]
10
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14/// A durable obligation, generated from a Signal, ready to be executed.
15/// Note the deliberate absence of `attempts`, `delay`, and `priority`.
16///
17/// Construct through [`Pact::new`]; the fields stay public for reading. The type is
18/// `#[non_exhaustive]` so it can gain a field in a later minor release without a
19/// breaking change.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[non_exhaustive]
22pub struct Pact {
23 /// Stable identifier for this pact.
24 pub id: Uuid,
25 /// Logical docket from which the pact can be claimed.
26 pub docket: String,
27 /// Application-defined pact kind.
28 pub kind: String,
29 /// Business data required to fulfill the pact.
30 pub clause: Vec<u8>,
31}
32
33impl Pact {
34 /// Build a pact from its identifier, docket, kind, and clause.
35 #[must_use]
36 pub fn new(id: Uuid, docket: String, kind: String, clause: Vec<u8>) -> Self {
37 Self {
38 id,
39 docket,
40 kind,
41 clause,
42 }
43 }
44}
45
46/// A retainer: the authority token a registry issues with a claim and validates
47/// when settling it. Authority is registry-validated — a forged identifier does not
48/// match an issued claim — not proven by the type system. Construct via
49/// [`Retainer::new`] and read the identifier via [`Retainer::id`]. Derives
50/// `PartialEq`/`Eq`/`Hash` so a durable backend can index lease state by holder
51/// identity — the orphan rule makes providing these the contract's responsibility.
52#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub struct Retainer(Uuid);
54
55impl Retainer {
56 /// Mint a retainer from an identifier. A registry issues tokens through this.
57 #[must_use]
58 pub fn new(id: Uuid) -> Self {
59 Self(id)
60 }
61
62 /// The retainer's identifier, which a registry validates on settlement.
63 #[must_use]
64 pub fn id(&self) -> Uuid {
65 self.0
66 }
67}
68
69/// A point in time as milliseconds since an epoch the runtime chooses. This is a
70/// pure value: the core names time but never reads it. There is deliberately no
71/// `now` constructor — a runtime obtains the current time and injects it, keeping
72/// lease decisions deterministic and testable.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
74pub struct Timestamp(u64);
75
76impl Timestamp {
77 /// Build a timestamp from milliseconds since the runtime's chosen epoch.
78 #[must_use]
79 pub fn from_millis(millis: u64) -> Self {
80 Self(millis)
81 }
82
83 /// The milliseconds since the runtime's chosen epoch.
84 #[must_use]
85 pub fn as_millis(self) -> u64 {
86 self.0
87 }
88
89 /// The timestamp `millis` milliseconds after this one, saturating at the maximum.
90 #[must_use]
91 pub fn plus_millis(self, millis: u64) -> Self {
92 Self(self.0.saturating_add(millis))
93 }
94}
95
96/// A claimed pact and the retainer required to settle it.
97///
98/// Construct through [`Claim::new`]; the fields stay public for reading. The type is
99/// `#[non_exhaustive]` so it can gain a field in a later minor release without a
100/// breaking change.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[non_exhaustive]
103pub struct Claim {
104 /// Pact claimed for execution.
105 pub pact: Pact,
106 /// Authority required to heartbeat, fulfill, or breach the claim.
107 pub retainer: Retainer,
108 /// When the claim's lease expires. After this the pact may be lapsed and
109 /// reclaimed unless the holder heartbeats first.
110 pub lease_expiry: Timestamp,
111}
112
113impl Claim {
114 /// Build a claim from a pact, the settling retainer, and the lease expiry.
115 #[must_use]
116 pub fn new(pact: Pact, retainer: Retainer, lease_expiry: Timestamp) -> Self {
117 Self {
118 pact,
119 retainer,
120 lease_expiry,
121 }
122 }
123}
124
125// The lease identity must be usable as a durable-backend key; removing the derives
126// fails this build rather than silently regressing the backend contract.
127const _: fn() = || {
128 fn assert_key<T: Eq + std::hash::Hash>() {}
129 assert_key::<Retainer>();
130};
131
132/// The lifecycle outcome an execution produces for a claimed pact.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum Outcome {
135 /// The pact was fulfilled successfully.
136 Fulfilled,
137 /// The pact could not be fulfilled and must be breached.
138 Breached,
139}
140
141/// The lifecycle conclusion applied to a claim, currently a fulfilled or breached
142/// [`Outcome`].
143pub type Settlement = Outcome;
144
145/// The pure lifecycle state machine every `Registry` backend composes over.
146///
147/// This is the single source of the pact lifecycle *semantics* — the claim-eligibility
148/// predicate, the state transitions, the current-holder authority check, and the lease
149/// arithmetic. A backend owns its own storage and mints its own retainer (a fencing
150/// value); it delegates every eligibility decision and transition here, so the semantics
151/// are defined once and cannot drift between backends (or between a synchronous and a
152/// future asynchronous binding).
153///
154/// It is colorless and sans-I/O: it reads no clock (time is an injected parameter),
155/// performs no I/O, and mints nothing non-deterministic (the retainer is supplied by the
156/// caller). Named `lifecycle` to distinguish it from the executor step-driver
157/// [`kernel`], which is a different pure machine.
158pub mod lifecycle {
159 use crate::{Retainer, Timestamp};
160
161 /// A pact's position in its claim lifecycle: the pure state a backend stores per
162 /// pact. The backend owns where it lives; this owns what it means.
163 #[derive(Debug, Clone, PartialEq, Eq)]
164 pub enum State {
165 /// Never claimed, or freshly seeded: immediately claimable.
166 Available,
167 /// Held under a lease by `retainer` until `expiry`. Claimable again only once
168 /// the lease has lapsed (`expiry < now`), which rotates authority away.
169 Held {
170 /// The current holder's authority token.
171 retainer: Retainer,
172 /// When the lease expires.
173 expiry: Timestamp,
174 },
175 /// Released non-terminally: claimable again only at or after `reclaimable_at`.
176 Deferred {
177 /// The instant at or after which the pact may be reclaimed.
178 reclaimable_at: Timestamp,
179 },
180 /// Concluded (fulfilled or breached): never claimable again.
181 Settled,
182 }
183
184 /// A transition was attempted by something that is not the state's current holder —
185 /// a stale retainer, or a state (available, deferred, settled) with no holder at all.
186 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
187 pub struct NotCurrentHolder;
188
189 impl std::fmt::Display for NotCurrentHolder {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 write!(f, "retainer is not the current holder of this pact")
192 }
193 }
194
195 impl std::error::Error for NotCurrentHolder {}
196
197 /// The lease expiry for a claim taken at `now` for `lease_millis` — the single
198 /// source of the lease arithmetic.
199 #[must_use]
200 pub fn lease_expiry(now: Timestamp, lease_millis: u64) -> Timestamp {
201 now.plus_millis(lease_millis)
202 }
203
204 /// Whether a pact in `state` may be claimed at `now`: the eligibility invariant.
205 /// `Available` always; a `Held` lease that has lapsed; a `Deferred` pact at or past
206 /// its instant; never a `Settled` one. Only positive, unambiguous eligibility.
207 #[must_use]
208 pub fn is_claimable(state: &State, now: Timestamp) -> bool {
209 match state {
210 State::Available => true,
211 State::Held { expiry, .. } => *expiry < now,
212 State::Deferred { reclaimable_at } => *reclaimable_at <= now,
213 State::Settled => false,
214 }
215 }
216
217 /// The state a successful claim produces: `Held` by `retainer` until the lease
218 /// expiry for `now`/`lease_millis`. The backend mints `retainer` and passes it in.
219 #[must_use]
220 pub fn on_claim(retainer: &Retainer, now: Timestamp, lease_millis: u64) -> State {
221 State::Held {
222 retainer: retainer.clone(),
223 expiry: lease_expiry(now, lease_millis),
224 }
225 }
226
227 /// The state a heartbeat produces: the lease extended to the expiry for
228 /// `now`/`lease_millis`, provided `retainer` currently holds `state` and the lease
229 /// has not already lapsed. A lapsed lease is not revived — the holder must re-claim.
230 pub fn on_heartbeat(
231 state: &State,
232 retainer: &Retainer,
233 now: Timestamp,
234 lease_millis: u64,
235 ) -> Result<State, NotCurrentHolder> {
236 match state {
237 State::Held {
238 retainer: held,
239 expiry,
240 } if held == retainer && *expiry >= now => Ok(State::Held {
241 retainer: retainer.clone(),
242 expiry: lease_expiry(now, lease_millis),
243 }),
244 _ => Err(NotCurrentHolder),
245 }
246 }
247
248 /// The state a settlement produces: `Settled`, provided `retainer` currently holds
249 /// `state`. Fulfill and breach share this — the lifecycle state records that the
250 /// obligation concluded, not which outcome concluded it.
251 pub fn on_settle(state: &State, retainer: &Retainer) -> Result<State, NotCurrentHolder> {
252 if is_current_holder(state, retainer) {
253 Ok(State::Settled)
254 } else {
255 Err(NotCurrentHolder)
256 }
257 }
258
259 /// The state a release produces: `Deferred` until `reclaimable_at`, provided
260 /// `retainer` currently holds `state`. Non-terminal; rotates authority away.
261 pub fn on_release(
262 state: &State,
263 retainer: &Retainer,
264 reclaimable_at: Timestamp,
265 ) -> Result<State, NotCurrentHolder> {
266 if is_current_holder(state, retainer) {
267 Ok(State::Deferred { reclaimable_at })
268 } else {
269 Err(NotCurrentHolder)
270 }
271 }
272
273 fn is_current_holder(state: &State, retainer: &Retainer) -> bool {
274 matches!(state, State::Held { retainer: held, .. } if held == retainer)
275 }
276
277 #[cfg(test)]
278 mod tests {
279 use super::*;
280 use uuid::Uuid;
281
282 fn retainer() -> Retainer {
283 Retainer::new(Uuid::new_v4())
284 }
285
286 #[test]
287 fn eligibility_covers_each_state() {
288 let now = Timestamp::from_millis(100);
289 assert!(is_claimable(&State::Available, now));
290 // A held lease is claimable only once lapsed.
291 assert!(!is_claimable(
292 &State::Held {
293 retainer: retainer(),
294 expiry: Timestamp::from_millis(101)
295 },
296 now
297 ));
298 assert!(is_claimable(
299 &State::Held {
300 retainer: retainer(),
301 expiry: Timestamp::from_millis(99)
302 },
303 now
304 ));
305 // A deferred pact is claimable at or past its instant.
306 assert!(!is_claimable(
307 &State::Deferred {
308 reclaimable_at: Timestamp::from_millis(101)
309 },
310 now
311 ));
312 assert!(is_claimable(
313 &State::Deferred {
314 reclaimable_at: Timestamp::from_millis(100)
315 },
316 now
317 ));
318 assert!(!is_claimable(&State::Settled, now));
319 }
320
321 #[test]
322 fn transitions_require_the_current_holder() {
323 let holder = retainer();
324 let held = State::Held {
325 retainer: holder.clone(),
326 expiry: Timestamp::from_millis(200),
327 };
328 let stranger = retainer();
329
330 assert_eq!(on_settle(&held, &stranger), Err(NotCurrentHolder));
331 assert_eq!(
332 on_release(&held, &stranger, Timestamp::from_millis(0)),
333 Err(NotCurrentHolder)
334 );
335 assert_eq!(on_settle(&State::Settled, &holder), Err(NotCurrentHolder));
336
337 assert_eq!(on_settle(&held, &holder), Ok(State::Settled));
338 assert_eq!(
339 on_release(&held, &holder, Timestamp::from_millis(500)),
340 Ok(State::Deferred {
341 reclaimable_at: Timestamp::from_millis(500)
342 })
343 );
344 }
345
346 #[test]
347 fn heartbeat_refreshes_but_does_not_revive_a_lapsed_lease() {
348 let holder = retainer();
349 let held = State::Held {
350 retainer: holder.clone(),
351 expiry: Timestamp::from_millis(200),
352 };
353 // Live lease refreshes.
354 assert_eq!(
355 on_heartbeat(&held, &holder, Timestamp::from_millis(150), 100),
356 Ok(State::Held {
357 retainer: holder.clone(),
358 expiry: Timestamp::from_millis(250)
359 })
360 );
361 // Lapsed lease is not revived.
362 assert_eq!(
363 on_heartbeat(&held, &holder, Timestamp::from_millis(201), 100),
364 Err(NotCurrentHolder)
365 );
366 }
367 }
368}
369
370/// A pure kernel transition decision — a [`lifecycle`] `on_X` — passed to the transition port
371/// [`Registry::apply`] (and its async twin). It is `Send + Sync` so the async binding's `apply`
372/// future stays `Send` across `.await`; the same type is used by both bindings, so the port is
373/// literally one shape.
374pub type Transition<'a> = dyn Fn(&lifecycle::State) -> Result<lifecycle::State, lifecycle::NotCurrentHolder>
375 + Send
376 + Sync
377 + 'a;
378
379/// The asynchronous binding of the [`Registry`] contract, available behind the `async` feature.
380/// [`AsyncRegistry`] is the same five-op contract over the same [`Transition`] port, made async;
381/// [`apply_via_cas`] is the optional compare-and-set helper. A consumer that does not enable `async`
382/// compiles none of it.
383#[cfg(feature = "async")]
384mod async_registry;
385#[cfg(feature = "async")]
386pub use async_registry::{AsyncRegistry, apply_via_cas};
387
388/// The Registry manages the lifecycle of Pacts. It is a pure state machine.
389///
390/// Time is injected: [`claim`](Registry::claim) and
391/// [`heartbeat`](Registry::heartbeat) take the current time as a parameter, and the
392/// registry reads no ambient clock. Settlement takes no time because a rotated
393/// retainer already tells a stale holder apart from the current one.
394pub trait Registry: Send + Sync {
395 /// Error returned by the registry implementation.
396 type Error: std::error::Error;
397
398 /// Claim a pact for execution from one of the requested dockets, using `now`
399 /// to set the new lease and to reclaim any pact whose lease already expired
400 /// without settlement — a lapse, realized through this normal claim path.
401 /// Reclaiming rotates the retainer, so the prior holder can no longer settle.
402 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error>;
403
404 /// The backend's lease duration in milliseconds, used by [`heartbeat`](Registry::heartbeat)
405 /// to compute the extended lease. Lease sizing is the backend's; the contract supplies the
406 /// mechanism, not a constant.
407 fn lease_millis(&self) -> u64;
408
409 /// Apply a lifecycle transition to the pact held by `retainer`, within the backend's own
410 /// atomic scope. `transition` is the pure kernel decision (a [`lifecycle`] `on_X`): the
411 /// backend loads the held state, computes the next state through `transition`, and applies
412 /// it atomically — it never decides the transition itself, so the lifecycle semantics stay
413 /// single-sourced in the kernel and cannot drift. A transition applied against a pact the
414 /// retainer no longer holds resolves to a not-current-holder error (the kernel's
415 /// [`NotCurrentHolder`](lifecycle::NotCurrentHolder) surfaces through `transition`). This is
416 /// the one transition port; the four transition operations below are provided over it.
417 ///
418 /// The backend owns *how* the scope is made atomic (a lock, a transaction, a native
419 /// conditional write, or compare-and-set); the contract mandates no concurrency-control
420 /// mechanism.
421 fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), Self::Error>;
422
423 /// Extend the retainer's lease using `now`. A heartbeat presented after the
424 /// lease already expired is rejected: the holder must claim again rather than
425 /// revive a lapsed lease, so two holders never both hold settlement authority.
426 fn heartbeat(&self, retainer: &Retainer, now: Timestamp) -> Result<(), Self::Error> {
427 let lease = self.lease_millis();
428 self.apply(retainer, &|state| {
429 lifecycle::on_heartbeat(state, retainer, now, lease)
430 })
431 }
432
433 /// Mark the pact as successfully fulfilled. Rejected when the retainer is not
434 /// the current holder.
435 fn fulfill(&self, retainer: &Retainer) -> Result<(), Self::Error> {
436 self.apply(retainer, &|state| lifecycle::on_settle(state, retainer))
437 }
438
439 /// Mark the pact as breached. Rejected when the retainer is not the current
440 /// holder. Shares the settlement transition with [`fulfill`](Registry::fulfill) — the
441 /// lifecycle records that the obligation concluded, not which outcome concluded it.
442 fn breach(&self, retainer: &Retainer) -> Result<(), Self::Error> {
443 self.apply(retainer, &|state| lifecycle::on_settle(state, retainer))
444 }
445
446 /// Release the claim without concluding the obligation, making the pact
447 /// reclaimable again only at or after `reclaimable_at`.
448 ///
449 /// This is **non-terminal**: unlike [`fulfill`](Registry::fulfill) and
450 /// [`breach`](Registry::breach), it settles nothing — the pact is left to be
451 /// attempted again. The registry computes no delay: `reclaimable_at` is a
452 /// consumer-supplied instant, honored exactly as the injected `now` is honored
453 /// (compared, never computed), so backoff policy stays with the caller and `Pact`
454 /// carries no delay. A `reclaimable_at` at or before now makes the pact immediately
455 /// claimable, as a voluntary lapse. Release rotates authority like a lapse, so the
456 /// prior retainer can no longer settle or heartbeat. Rejected when the retainer is
457 /// not the current holder.
458 fn release(&self, retainer: &Retainer, reclaimable_at: Timestamp) -> Result<(), Self::Error> {
459 self.apply(retainer, &|state| {
460 lifecycle::on_release(state, retainer, reclaimable_at)
461 })
462 }
463}
464
465/// The sans-I/O lifecycle kernel.
466///
467/// The kernel is a pure state machine: it decides the next [`Directive`](kernel::Directive)
468/// from its state and absorbs [`Notice`](kernel::Notice) reports a runtime feeds
469/// back. It performs no I/O
470/// and exposes no `async fn`, so it commits to no runtime shape. It encodes the
471/// lifecycle decision table only — it adds no orchestration behavior.
472///
473/// # Advanced surface
474///
475/// This is the **advanced** tier of Pacta's public API: lower stability intent than
476/// the recommended surface (its API may evolve as the runtime story settles), though
477/// it stays a supported, governed core surface — not unsupported or slated for
478/// removal. Most consumers should compose with the `Driver` runtime (or the `pacta`
479/// facade) and never touch the kernel. Reach for it only to build a custom runtime;
480/// it is reached through `pacta-contract` directly, never through the `pacta` facade.
481///
482/// # Driving the kernel
483///
484/// A runtime drives one lifecycle step by looping: ask [`poll`](kernel::Kernel::poll)
485/// for the next [`Directive`](kernel::Directive), perform it, report the outcome back with
486/// [`on_event`](kernel::Kernel::on_event), and repeat until [`result`](kernel::Kernel::result)
487/// yields a terminal [`StepResult`](kernel::StepResult). The kernel decides *what*;
488/// the runtime performs it and injects time — the kernel reads no clock.
489///
490/// ```
491/// use pacta_contract::kernel::{Directive, Kernel, Notice, StepResult};
492/// use pacta_contract::{Claim, Outcome, Pact, Retainer, Timestamp};
493///
494/// let mut kernel = Kernel::new();
495/// let mut available = Some(Claim::new(
496/// Pact::new(Default::default(), "demo".into(), "demo".into(), Vec::new()),
497/// Retainer::new(Default::default()),
498/// Timestamp::from_millis(0),
499/// ));
500///
501/// let result = loop {
502/// if let Some(result) = kernel.result() {
503/// break result;
504/// }
505/// match kernel.poll() {
506/// Directive::Claim => kernel.on_event(Notice::Claimed(available.take())),
507/// Directive::Execute(_pact) => kernel.on_event(Notice::Executed(Outcome::Fulfilled)),
508/// Directive::Settle(_retainer, _outcome) => kernel.on_event(Notice::Settled),
509/// Directive::Idle => break StepResult::Idle,
510/// _ => unreachable!("driver handles every current kernel directive"),
511/// }
512/// };
513///
514/// assert_eq!(result, StepResult::Settled(Outcome::Fulfilled));
515/// ```
516pub mod kernel {
517 use crate::{Claim, Outcome, Pact, Retainer};
518
519 /// An instruction the kernel issues for a runtime to perform.
520 ///
521 /// `#[non_exhaustive]`: this advanced-tier protocol may gain directives, so a
522 /// runtime's match must carry a wildcard arm.
523 #[derive(Debug, Clone)]
524 #[non_exhaustive]
525 pub enum Directive {
526 /// Claim a pact from the runtime's configured dockets.
527 Claim,
528 /// Execute the given claimed pact.
529 Execute(Pact),
530 /// Settle the claim identified by the retainer with the decided outcome.
531 Settle(Retainer, Outcome),
532 /// Nothing remains to be performed for this step.
533 Idle,
534 }
535
536 /// A report a runtime feeds back after performing a [`Directive`].
537 ///
538 /// `#[non_exhaustive]`: this advanced-tier protocol may gain notices, so a
539 /// consumer's match must carry a wildcard arm.
540 #[derive(Debug, Clone)]
541 #[non_exhaustive]
542 pub enum Notice {
543 /// Result of a claim: a claim if one was available, else none.
544 Claimed(Option<Claim>),
545 /// An execution produced a lifecycle outcome.
546 Executed(Outcome),
547 /// The execution infrastructure failed to run the pact — the executor
548 /// produced no outcome. The kernel fabricates no outcome for this notice: it
549 /// settles nothing and reaches an unsettled terminal, leaving the claim to
550 /// lapse and be reclaimed, while the runtime surfaces the error to its caller.
551 ExecutionFailed,
552 /// A settlement was persisted.
553 Settled,
554 }
555
556 /// The terminal result of one lifecycle step.
557 ///
558 /// `#[non_exhaustive]`: this advanced-tier protocol may gain results, so a
559 /// consumer's match must carry a wildcard arm.
560 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
561 #[non_exhaustive]
562 pub enum StepResult {
563 /// No pact was available to claim.
564 Idle,
565 /// The claim was settled with this outcome.
566 Settled(Outcome),
567 /// Execution produced no outcome (an infrastructure failure), so nothing was
568 /// settled. The claim is left held-but-unsettled to lapse and be reclaimed;
569 /// the kernel fabricates no `Outcome` from the absence of one.
570 Unsettled,
571 }
572
573 #[derive(Debug)]
574 enum Phase {
575 Claiming,
576 Executing {
577 pact: Pact,
578 retainer: Retainer,
579 },
580 Settling {
581 retainer: Retainer,
582 outcome: Outcome,
583 },
584 DoneIdle,
585 DoneSettled(Outcome),
586 DoneUnsettled,
587 }
588
589 /// The pure lifecycle state machine for a single step.
590 #[derive(Debug)]
591 pub struct Kernel {
592 phase: Phase,
593 }
594
595 impl Kernel {
596 /// Start a fresh lifecycle step.
597 #[must_use]
598 pub fn new() -> Self {
599 Self {
600 phase: Phase::Claiming,
601 }
602 }
603
604 /// Decide the next directive from the current state.
605 #[must_use]
606 pub fn poll(&self) -> Directive {
607 match &self.phase {
608 Phase::Claiming => Directive::Claim,
609 Phase::Executing { pact, .. } => Directive::Execute(pact.clone()),
610 Phase::Settling { retainer, outcome } => {
611 Directive::Settle(retainer.clone(), *outcome)
612 }
613 Phase::DoneIdle | Phase::DoneSettled(_) | Phase::DoneUnsettled => Directive::Idle,
614 }
615 }
616
617 /// Absorb a runtime report, advancing the lifecycle.
618 pub fn on_event(&mut self, notice: Notice) {
619 let phase = std::mem::replace(&mut self.phase, Phase::DoneIdle);
620 self.phase = match (phase, notice) {
621 (Phase::Claiming, Notice::Claimed(Some(claim))) => Phase::Executing {
622 pact: claim.pact,
623 retainer: claim.retainer,
624 },
625 (Phase::Claiming, Notice::Claimed(None)) => Phase::DoneIdle,
626 (Phase::Executing { retainer, .. }, Notice::Executed(outcome)) => {
627 Phase::Settling { retainer, outcome }
628 }
629 (Phase::Executing { .. }, Notice::ExecutionFailed) => Phase::DoneUnsettled,
630 (Phase::Settling { outcome, .. }, Notice::Settled) => Phase::DoneSettled(outcome),
631 (other, _) => other,
632 };
633 }
634
635 /// The terminal result once the step reaches a terminal state, else `None`.
636 #[must_use]
637 pub fn result(&self) -> Option<StepResult> {
638 match &self.phase {
639 Phase::DoneIdle => Some(StepResult::Idle),
640 Phase::DoneSettled(outcome) => Some(StepResult::Settled(*outcome)),
641 Phase::DoneUnsettled => Some(StepResult::Unsettled),
642 _ => None,
643 }
644 }
645 }
646
647 impl Default for Kernel {
648 fn default() -> Self {
649 Self::new()
650 }
651 }
652
653 #[cfg(test)]
654 mod tests {
655 use super::*;
656 use crate::Timestamp;
657 use uuid::Uuid;
658
659 fn claim() -> Claim {
660 Claim::new(
661 Pact::new(
662 Uuid::new_v4(),
663 "default".to_string(),
664 "example".to_string(),
665 Vec::new(),
666 ),
667 Retainer::new(Uuid::new_v4()),
668 Timestamp::from_millis(0),
669 )
670 }
671
672 fn drive(
673 kernel: &mut Kernel,
674 execution: Result<Outcome, ()>,
675 claimable: bool,
676 ) -> StepResult {
677 loop {
678 if let Some(result) = kernel.result() {
679 return result;
680 }
681 match kernel.poll() {
682 Directive::Claim => {
683 let notice = if claimable {
684 Notice::Claimed(Some(claim()))
685 } else {
686 Notice::Claimed(None)
687 };
688 kernel.on_event(notice);
689 }
690 Directive::Execute(_) => kernel.on_event(match execution {
691 Ok(outcome) => Notice::Executed(outcome),
692 Err(()) => Notice::ExecutionFailed,
693 }),
694 Directive::Settle(_, _) => kernel.on_event(Notice::Settled),
695 Directive::Idle => return StepResult::Idle,
696 }
697 }
698 }
699
700 #[test]
701 fn fulfilled_execution_settles_fulfilled() {
702 let mut kernel = Kernel::new();
703 assert_eq!(
704 drive(&mut kernel, Ok(Outcome::Fulfilled), true),
705 StepResult::Settled(Outcome::Fulfilled)
706 );
707 }
708
709 #[test]
710 fn breached_execution_settles_breached() {
711 let mut kernel = Kernel::new();
712 assert_eq!(
713 drive(&mut kernel, Ok(Outcome::Breached), true),
714 StepResult::Settled(Outcome::Breached)
715 );
716 }
717
718 #[test]
719 fn infrastructure_error_is_unsettled() {
720 let mut kernel = Kernel::new();
721 assert_eq!(drive(&mut kernel, Err(()), true), StepResult::Unsettled);
722 }
723
724 #[test]
725 fn empty_claim_is_idle() {
726 let mut kernel = Kernel::new();
727 assert_eq!(
728 drive(&mut kernel, Ok(Outcome::Fulfilled), false),
729 StepResult::Idle
730 );
731 }
732 }
733}