Skip to main content

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}