1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//! Uniform node I/O contract types (spec §4.2–§4.4).
//!
//! - [`NodeOutput`] — a node's output, **mutually exclusive** `{data}` (success)
//! or `{errors}` (failure), per JSON:API / JSON-RPC 2.0.
//! - [`NodeError`] — a Temporal-shaped error object (`code`/`message`/`kind`/
//! `retryable`/`source`/`details`).
//! - [`InputValue`] / [`StructuredInput`] — explicit literal-vs-ref-vs-template
//! inputs (Step Functions lesson: encode it structurally, never sniff delimiters).
//! - [`FlowState`] — the per-node output namespace, keyed by node id (n8n model).
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde_json::Value;
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Coarse, routing/retry-oriented classification of a [`NodeError`]. This is the
/// branch/retry key (Step Functions `ErrorEquals`, Temporal failure `type`);
/// `code` stays the canonical open-ended string.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum ErrorKind {
/// Input failed validation.
Validation,
/// Operation timed out.
Timeout,
/// Authentication / authorization failure.
Auth,
/// Required entity not found.
NotFound,
/// Throttled by rate limits.
RateLimited,
/// Unclassified internal failure.
Internal,
/// Operation cancelled (e.g. a user cancelled a form/QA step).
Cancelled,
}
/// Where an error originated — the node and (optionally) the input that failed.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct NodeErrorSource {
/// The node id that produced the error.
pub node: String,
/// The input name that triggered it, when applicable.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub input: Option<String>,
}
/// A single error in a node's `{errors}` envelope.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct NodeError {
/// Stable, author/UI-facing error code (open-ended string, per JSON:API).
pub code: String,
/// Required human-readable message (one concise sentence).
pub message: String,
/// Coarse routing/retry classification.
pub kind: ErrorKind,
/// Explicit retryability — never inferred from `kind` (Temporal's `non_retryable`).
pub retryable: bool,
/// Optional provenance.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub source: Option<NodeErrorSource>,
/// Free-form structured detail; omitted from the wire when null.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Value::is_null")
)]
pub details: Value,
}
/// A node's output: success carries `{data}`, failure carries `{errors}`. The two
/// are mutually exclusive (enforced by the type); engine metadata lives in a
/// sibling `meta`, never inside `data`.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum NodeOutput {
/// Success: arbitrary node-defined fields under `data`.
Data {
/// The success payload.
data: Value,
},
/// Failure: one or more errors under `errors`.
Errors {
/// The error list (a node may report several validation failures at once).
errors: Vec<NodeError>,
},
}
impl NodeOutput {
/// Build a success output `{ data }`.
pub fn ok(data: Value) -> Self {
Self::Data { data }
}
/// Build a failure output `{ errors }`.
pub fn failed(errors: Vec<NodeError>) -> Self {
Self::Errors { errors }
}
/// `true` when this is a success (`{data}`) output.
pub fn is_ok(&self) -> bool {
matches!(self, Self::Data { .. })
}
/// The success payload, or `None` on a failure output.
pub fn data(&self) -> Option<&Value> {
match self {
Self::Data { data } => Some(data),
Self::Errors { .. } => None,
}
}
/// The error list, or an empty slice on a success output.
pub fn errors(&self) -> &[NodeError] {
match self {
Self::Errors { errors } => errors,
Self::Data { .. } => &[],
}
}
}
/// An explicit node input value — resolved structurally, never by sniffing
/// delimiters. Serializes to a single-key object: `{literal|ref|template}`.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum InputValue {
/// Used as-is.
Literal(Value),
/// A single addressable path (`node.<id>.data.<field>`), resolved to its typed value.
Ref(String),
/// String interpolation; always yields a string.
Template(String),
}
/// A named node input (`{ name, value }`).
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct StructuredInput {
/// The input parameter name.
pub name: String,
/// Its value: literal, reference, or template.
pub value: InputValue,
}
/// The per-node output namespace: each node's [`NodeOutput`] kept immutable,
/// keyed by its stable node id. Downstream addresses `<node_id>.data.<path>`.
pub type FlowState = BTreeMap<String, NodeOutput>;