af_workflow/result.rs
1//! What a step node returns. Port of `platform/types.py` Pass / Drop / FanOut.
2
3use chrono::{DateTime, Utc};
4use serde_json::Value;
5
6use crate::{event::Event, ResourceReservationRef};
7
8/// Product-owned action data emitted by an `execute.*` step. The durable
9/// driver adds tenant, run, lease, control and exact capability pins.
10#[derive(Debug, Clone, PartialEq)]
11pub struct PreparedAction {
12 /// Capability id the intent will pin.
13 pub capability_id: String,
14 /// Product idempotency key; the driver scopes it to the instance and state version.
15 pub idempotency_key: String,
16 /// Structured input handed to the provider.
17 pub input: Value,
18 /// Resource scope the action competes in (empty when unscoped).
19 pub resource_scope_id: String,
20 /// Fenced reservation for funds actions.
21 pub reservation: Option<ResourceReservationRef>,
22 /// Latest time by which the action must finish.
23 pub deadline: Option<DateTime<Utc>>,
24}
25
26impl PreparedAction {
27 /// Unscoped action with no reservation or deadline.
28 pub fn new(
29 capability_id: impl Into<String>,
30 idempotency_key: impl Into<String>,
31 input: Value,
32 ) -> Self {
33 Self {
34 capability_id: capability_id.into(),
35 idempotency_key: idempotency_key.into(),
36 input,
37 resource_scope_id: String::new(),
38 reservation: None,
39 deadline: None,
40 }
41 }
42
43 /// Reject blank capability or idempotency identifiers.
44 pub fn validate(&self) -> Result<(), String> {
45 if self.capability_id.trim().is_empty() || self.idempotency_key.trim().is_empty() {
46 return Err("prepared action capability_id and idempotency_key are required".into());
47 }
48 Ok(())
49 }
50}
51
52/// The outcome of running one event through one [`crate::node::StepNode`].
53#[derive(Debug, Clone)]
54pub enum StepResult {
55 /// Forward the (possibly derived) event to the next step.
56 Pass(Event),
57 /// Stop propagation here. Filters and sink terminals return this.
58 Drop {
59 /// Why the event stopped.
60 reason: String,
61 /// Stable machine-readable code for material business outcomes. A
62 /// routine filter drop leaves this empty.
63 exit_reason: Option<String>,
64 },
65 /// Forward several events. Only nodes that declare `produces_fan_out`
66 /// may return this; the validator bans fan-out upstream of `execute.*`.
67 FanOut(Vec<Event>),
68 /// Persist an external action intent and continue with the supplied event.
69 /// The step never invokes the provider inline.
70 Action {
71 /// Event forwarded to the next step.
72 event: Event,
73 /// The effect to persist as an intent.
74 action: Box<PreparedAction>,
75 },
76}
77
78impl StepResult {
79 /// A routine drop that recorders do not persist as a step.
80 pub fn drop(reason: impl Into<String>) -> Self {
81 StepResult::Drop {
82 reason: reason.into(),
83 exit_reason: None,
84 }
85 }
86
87 /// A material gate/drop that recorders should persist as a run step.
88 pub fn gate(exit_reason: impl Into<String>) -> Self {
89 let code = exit_reason.into();
90 StepResult::Drop {
91 reason: code.clone(),
92 exit_reason: Some(code),
93 }
94 }
95}