Skip to main content

greentic_types/
flow.rs

1//! Unified flow model used by packs and runtimes.
2
3use alloc::collections::{BTreeMap, BTreeSet};
4use alloc::string::String;
5use core::hash::BuildHasherDefault;
6
7use fnv::FnvHasher;
8use indexmap::IndexMap;
9use serde_json::Value;
10
11use crate::{ComponentId, FlowId, NodeId};
12
13/// Build hasher used for flow node maps (Fnv for `no_std` friendliness).
14pub type FlowHasher = BuildHasherDefault<FnvHasher>;
15
16#[cfg(feature = "schemars")]
17use schemars::JsonSchema;
18#[cfg(feature = "serde")]
19use serde::{Deserialize, Serialize};
20
21/// Supported flow kinds across Greentic packs.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
25#[cfg_attr(feature = "schemars", derive(JsonSchema))]
26pub enum FlowKind {
27    /// Inbound messaging flows (Telegram, Teams, HTTP chat).
28    Messaging,
29    /// Event-driven flows (webhooks, NATS, cron, etc.).
30    Event,
31    /// Flows that configure components/providers/infrastructure.
32    ComponentConfig,
33    /// Batch/background jobs.
34    Job,
35    /// HTTP-style request/response flows.
36    Http,
37}
38
39/// Canonical flow representation embedded in packs.
40#[derive(Clone, Debug, PartialEq)]
41#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
42#[cfg_attr(
43    feature = "schemars",
44    derive(JsonSchema),
45    schemars(
46        title = "Greentic Flow v1",
47        description = "Canonical flow model with components, routing and telemetry.",
48        rename = "greentic.flow.v1"
49    )
50)]
51pub struct Flow {
52    /// Schema version for the flow document.
53    pub schema_version: String,
54    /// Flow identifier inside the pack.
55    pub id: FlowId,
56    /// Flow execution kind.
57    pub kind: FlowKind,
58    /// Entrypoints for this flow keyed by name (for example `default`, `telegram`, `http:/path`).
59    #[cfg_attr(feature = "serde", serde(default))]
60    #[cfg_attr(
61        feature = "schemars",
62        schemars(with = "alloc::collections::BTreeMap<String, Value>")
63    )]
64    pub entrypoints: BTreeMap<String, Value>,
65    /// Ordered node map describing the flow graph.
66    #[cfg_attr(feature = "serde", serde(default))]
67    #[cfg_attr(
68        feature = "schemars",
69        schemars(with = "alloc::collections::BTreeMap<NodeId, Node>")
70    )]
71    pub nodes: IndexMap<NodeId, Node, FlowHasher>,
72    /// Optional metadata for authoring/UX.
73    #[cfg_attr(feature = "serde", serde(default))]
74    pub metadata: FlowMetadata,
75}
76
77impl Flow {
78    /// Returns `true` when no nodes are defined.
79    pub fn is_empty(&self) -> bool {
80        self.nodes.is_empty()
81    }
82
83    /// Returns the implicit ingress node (first user-declared entry).
84    pub fn ingress(&self) -> Option<(&NodeId, &Node)> {
85        self.nodes.iter().next()
86    }
87}
88
89/// Flow node representation.
90#[derive(Clone, Debug, PartialEq)]
91#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
92#[cfg_attr(feature = "schemars", derive(JsonSchema))]
93pub struct Node {
94    /// Node identifier.
95    pub id: NodeId,
96    /// Component binding referenced by the node.
97    pub component: ComponentRef,
98    /// Component input mapping configuration.
99    #[cfg_attr(feature = "serde", serde(alias = "in_map"))]
100    pub input: InputMapping,
101    /// Component output mapping configuration.
102    #[cfg_attr(feature = "serde", serde(alias = "out_map"))]
103    pub output: OutputMapping,
104    /// Optional error mapping configuration.
105    #[cfg_attr(
106        feature = "serde",
107        serde(
108            default,
109            skip_serializing_if = "Option::is_none",
110            rename = "err_map",
111            alias = "error_output"
112        )
113    )]
114    pub err_map: Option<OutputMapping>,
115    /// Routing behaviour after this node.
116    pub routing: Routing,
117    /// Optional telemetry hints for this node.
118    #[cfg_attr(feature = "serde", serde(default))]
119    pub telemetry: TelemetryHints,
120}
121
122impl Node {
123    /// Returns the canonical input mapping surface.
124    pub fn in_map(&self) -> &InputMapping {
125        &self.input
126    }
127
128    /// Returns the canonical output mapping surface.
129    pub fn out_map(&self) -> &OutputMapping {
130        &self.output
131    }
132
133    /// Returns the canonical error mapping surface when configured.
134    pub fn err_map(&self) -> Option<&OutputMapping> {
135        self.err_map.as_ref()
136    }
137}
138
139/// Component reference within a flow.
140#[derive(Clone, Debug, PartialEq)]
141#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
142#[cfg_attr(feature = "schemars", derive(JsonSchema))]
143pub struct ComponentRef {
144    /// Component identifier.
145    pub id: ComponentId,
146    /// Dependency pack alias when referencing external components.
147    #[cfg_attr(
148        feature = "serde",
149        serde(default, skip_serializing_if = "Option::is_none")
150    )]
151    pub pack_alias: Option<String>,
152    /// Optional operation name within the component.
153    #[cfg_attr(
154        feature = "serde",
155        serde(default, skip_serializing_if = "Option::is_none")
156    )]
157    pub operation: Option<String>,
158}
159
160/// Opaque component input mapping configuration.
161#[derive(Clone, Debug, PartialEq)]
162#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
163#[cfg_attr(feature = "schemars", derive(JsonSchema))]
164pub struct InputMapping {
165    /// Mapping configuration (templates, expressions, etc.).
166    #[cfg_attr(feature = "serde", serde(default))]
167    pub mapping: Value,
168}
169
170/// Opaque component output mapping configuration.
171#[derive(Clone, Debug, PartialEq)]
172#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
173#[cfg_attr(feature = "schemars", derive(JsonSchema))]
174pub struct OutputMapping {
175    /// Mapping configuration (templates, expressions, etc.).
176    #[cfg_attr(feature = "serde", serde(default))]
177    pub mapping: Value,
178}
179
180/// Optional authoring metadata for flows.
181#[derive(Clone, Debug, PartialEq)]
182#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
183#[cfg_attr(feature = "schemars", derive(JsonSchema))]
184pub struct FlowMetadata {
185    /// Optional human-friendly title.
186    #[cfg_attr(
187        feature = "serde",
188        serde(default, skip_serializing_if = "Option::is_none")
189    )]
190    pub title: Option<String>,
191    /// Optional human-friendly description.
192    #[cfg_attr(
193        feature = "serde",
194        serde(default, skip_serializing_if = "Option::is_none")
195    )]
196    pub description: Option<String>,
197    /// Optional tags.
198    #[cfg_attr(feature = "serde", serde(default))]
199    pub tags: BTreeSet<String>,
200    /// Free-form metadata.
201    #[cfg_attr(feature = "serde", serde(default))]
202    pub extra: Value,
203}
204
205impl Default for FlowMetadata {
206    fn default() -> Self {
207        Self {
208            title: None,
209            description: None,
210            tags: BTreeSet::new(),
211            extra: Value::Null,
212        }
213    }
214}
215
216/// Routing behaviour for a node.
217#[derive(Clone, Debug, PartialEq)]
218#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
219#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
220#[cfg_attr(feature = "schemars", derive(JsonSchema))]
221pub enum Routing {
222    /// Continue to the specified node.
223    Next {
224        /// Destination node identifier.
225        node_id: NodeId,
226    },
227    /// Branch based on status string -> node id.
228    Branch {
229        /// Mapping of status value to destination node.
230        #[cfg_attr(feature = "serde", serde(default))]
231        #[cfg_attr(
232            feature = "schemars",
233            schemars(with = "alloc::collections::BTreeMap<String, NodeId>")
234        )]
235        on_status: BTreeMap<String, NodeId>,
236        /// Default node when no status matches.
237        #[cfg_attr(
238            feature = "serde",
239            serde(default, skip_serializing_if = "Option::is_none")
240        )]
241        default: Option<NodeId>,
242    },
243    /// Flow terminates successfully.
244    End,
245    /// Reply to origin (Messaging/Http flows).
246    Reply,
247    /// Component- or runtime-specific routing.
248    Custom(Value),
249}
250
251/// Optional telemetry hints for a node.
252#[derive(Clone, Debug, Default, PartialEq, Eq)]
253#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
254#[cfg_attr(feature = "schemars", derive(JsonSchema))]
255pub struct TelemetryHints {
256    /// Optional span name.
257    #[cfg_attr(
258        feature = "serde",
259        serde(default, skip_serializing_if = "Option::is_none")
260    )]
261    pub span_name: Option<String>,
262    /// Attributes to attach to spans/logs.
263    #[cfg_attr(feature = "serde", serde(default))]
264    pub attributes: BTreeMap<String, String>,
265    /// Sampling hint (`high`, `normal`, `low`).
266    #[cfg_attr(
267        feature = "serde",
268        serde(default, skip_serializing_if = "Option::is_none")
269    )]
270    pub sampling: Option<String>,
271}