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    pub input: InputMapping,
100    /// Component output mapping configuration.
101    pub output: OutputMapping,
102    /// Routing behaviour after this node.
103    pub routing: Routing,
104    /// Optional telemetry hints for this node.
105    #[cfg_attr(feature = "serde", serde(default))]
106    pub telemetry: TelemetryHints,
107}
108
109/// Component reference within a flow.
110#[derive(Clone, Debug, PartialEq)]
111#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
112#[cfg_attr(feature = "schemars", derive(JsonSchema))]
113pub struct ComponentRef {
114    /// Component identifier.
115    pub id: ComponentId,
116    /// Dependency pack alias when referencing external components.
117    #[cfg_attr(
118        feature = "serde",
119        serde(default, skip_serializing_if = "Option::is_none")
120    )]
121    pub pack_alias: Option<String>,
122    /// Optional operation name within the component.
123    #[cfg_attr(
124        feature = "serde",
125        serde(default, skip_serializing_if = "Option::is_none")
126    )]
127    pub operation: Option<String>,
128}
129
130/// Opaque component input mapping configuration.
131#[derive(Clone, Debug, PartialEq)]
132#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
133#[cfg_attr(feature = "schemars", derive(JsonSchema))]
134pub struct InputMapping {
135    /// Mapping configuration (templates, expressions, etc.).
136    #[cfg_attr(feature = "serde", serde(default))]
137    pub mapping: Value,
138}
139
140/// Opaque component output mapping configuration.
141#[derive(Clone, Debug, PartialEq)]
142#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
143#[cfg_attr(feature = "schemars", derive(JsonSchema))]
144pub struct OutputMapping {
145    /// Mapping configuration (templates, expressions, etc.).
146    #[cfg_attr(feature = "serde", serde(default))]
147    pub mapping: Value,
148}
149
150/// Optional authoring metadata for flows.
151#[derive(Clone, Debug, PartialEq)]
152#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
153#[cfg_attr(feature = "schemars", derive(JsonSchema))]
154pub struct FlowMetadata {
155    /// Optional human-friendly title.
156    #[cfg_attr(
157        feature = "serde",
158        serde(default, skip_serializing_if = "Option::is_none")
159    )]
160    pub title: Option<String>,
161    /// Optional human-friendly description.
162    #[cfg_attr(
163        feature = "serde",
164        serde(default, skip_serializing_if = "Option::is_none")
165    )]
166    pub description: Option<String>,
167    /// Optional tags.
168    #[cfg_attr(feature = "serde", serde(default))]
169    pub tags: BTreeSet<String>,
170    /// Free-form metadata.
171    #[cfg_attr(feature = "serde", serde(default))]
172    pub extra: Value,
173}
174
175impl Default for FlowMetadata {
176    fn default() -> Self {
177        Self {
178            title: None,
179            description: None,
180            tags: BTreeSet::new(),
181            extra: Value::Null,
182        }
183    }
184}
185
186/// Routing behaviour for a node.
187#[derive(Clone, Debug, PartialEq)]
188#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
189#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
190#[cfg_attr(feature = "schemars", derive(JsonSchema))]
191pub enum Routing {
192    /// Continue to the specified node.
193    Next {
194        /// Destination node identifier.
195        node_id: NodeId,
196    },
197    /// Branch based on status string -> node id.
198    Branch {
199        /// Mapping of status value to destination node.
200        #[cfg_attr(feature = "serde", serde(default))]
201        #[cfg_attr(
202            feature = "schemars",
203            schemars(with = "alloc::collections::BTreeMap<String, NodeId>")
204        )]
205        on_status: BTreeMap<String, NodeId>,
206        /// Default node when no status matches.
207        #[cfg_attr(
208            feature = "serde",
209            serde(default, skip_serializing_if = "Option::is_none")
210        )]
211        default: Option<NodeId>,
212    },
213    /// Flow terminates successfully.
214    End,
215    /// Reply to origin (Messaging/Http flows).
216    Reply,
217    /// Component- or runtime-specific routing.
218    Custom(Value),
219}
220
221/// Optional telemetry hints for a node.
222#[derive(Clone, Debug, Default, PartialEq, Eq)]
223#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
224#[cfg_attr(feature = "schemars", derive(JsonSchema))]
225pub struct TelemetryHints {
226    /// Optional span name.
227    #[cfg_attr(
228        feature = "serde",
229        serde(default, skip_serializing_if = "Option::is_none")
230    )]
231    pub span_name: Option<String>,
232    /// Attributes to attach to spans/logs.
233    #[cfg_attr(feature = "serde", serde(default))]
234    pub attributes: BTreeMap<String, String>,
235    /// Sampling hint (`high`, `normal`, `low`).
236    #[cfg_attr(
237        feature = "serde",
238        serde(default, skip_serializing_if = "Option::is_none")
239    )]
240    pub sampling: Option<String>,
241}