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