greentic_types/
flow.rs

1//! Generic flow graph definitions used by packs and runtimes.
2
3use alloc::string::String;
4use core::hash::BuildHasherDefault;
5
6use fnv::FnvHasher;
7use indexmap::IndexMap;
8use serde_json::Value;
9
10use crate::{ComponentId, FlowId, NodeId, component::ComponentManifest};
11
12/// Build hasher used for flow node maps (Fnv for `no_std` friendliness).
13type FlowHasher = BuildHasherDefault<FnvHasher>;
14
15/// Ordered node container referenced by [`Flow`].
16pub type FlowNodes = IndexMap<NodeId, Node, FlowHasher>;
17
18#[cfg(feature = "schemars")]
19use schemars::JsonSchema;
20#[cfg(feature = "serde")]
21use serde::{Deserialize, Serialize};
22
23/// Supported flow kinds.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
27#[cfg_attr(feature = "schemars", derive(JsonSchema))]
28pub enum FlowKind {
29    /// Session-centric messaging flows (chat, DM, etc.).
30    Messaging,
31    /// Fire-and-forget event flows (webhooks, timers, etc.).
32    Events,
33}
34
35/// Canonical .ygtc flow representation.
36#[derive(Clone, Debug, PartialEq)]
37#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
38#[cfg_attr(feature = "schemars", derive(JsonSchema))]
39pub struct Flow {
40    /// Flow execution kind.
41    pub kind: FlowKind,
42    /// Flow identifier inside the pack.
43    pub id: FlowId,
44    /// Optional human-friendly summary.
45    #[cfg_attr(
46        feature = "serde",
47        serde(default, skip_serializing_if = "Option::is_none")
48    )]
49    pub description: Option<String>,
50    /// Ordered node map describing the flow graph.
51    #[cfg_attr(feature = "serde", serde(default))]
52    #[cfg_attr(
53        feature = "schemars",
54        schemars(with = "alloc::collections::BTreeMap<NodeId, Node>")
55    )]
56    pub nodes: FlowNodes,
57}
58
59impl Flow {
60    /// Returns `true` when no nodes are defined.
61    pub fn is_empty(&self) -> bool {
62        self.nodes.is_empty()
63    }
64
65    /// Returns the implicit ingress node (first user-declared entry).
66    pub fn ingress(&self) -> Option<(&NodeId, &Node)> {
67        self.nodes.iter().next()
68    }
69
70    /// Validates the flow structure (at least one node).
71    pub fn validate_structure(&self) -> Result<(), FlowValidationError> {
72        if self.is_empty() {
73            return Err(FlowValidationError::EmptyFlow);
74        }
75        Ok(())
76    }
77
78    /// Ensures all referenced components exist and support this flow kind.
79    pub fn validate_components<'a, F>(&self, mut resolver: F) -> Result<(), FlowValidationError>
80    where
81        F: FnMut(&ComponentId) -> Option<&'a ComponentManifest>,
82    {
83        self.validate_structure()?;
84        for (node_id, node) in &self.nodes {
85            if let Some(component_id) = &node.component {
86                let manifest = resolver(component_id).ok_or_else(|| {
87                    FlowValidationError::MissingComponent {
88                        node_id: node_id.clone(),
89                        component: component_id.clone(),
90                    }
91                })?;
92
93                if !manifest.supports_kind(self.kind) {
94                    return Err(FlowValidationError::UnsupportedComponent {
95                        node_id: node_id.clone(),
96                        component: component_id.clone(),
97                        flow_kind: self.kind,
98                    });
99                }
100            }
101        }
102        Ok(())
103    }
104}
105
106/// Flow node metadata. All semantics are opaque strings or documents.
107#[derive(Clone, Debug, PartialEq)]
108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
109#[cfg_attr(feature = "schemars", derive(JsonSchema))]
110pub struct Node {
111    /// Component kind (opaque string interpreted by tooling/runtime).
112    pub kind: String,
113    /// Optional profile override for this node.
114    #[cfg_attr(
115        feature = "serde",
116        serde(default, skip_serializing_if = "Option::is_none")
117    )]
118    pub profile: Option<String>,
119    /// Optional component binding identifier.
120    #[cfg_attr(
121        feature = "serde",
122        serde(default, skip_serializing_if = "Option::is_none")
123    )]
124    pub component: Option<ComponentId>,
125    /// Component-specific configuration blob.
126    #[cfg_attr(feature = "serde", serde(default))]
127    pub config: Value,
128    /// Opaque routing document interpreted by the component.
129    #[cfg_attr(feature = "serde", serde(default))]
130    pub routing: Value,
131}
132
133/// Validation errors produced by [`Flow::validate_components`].
134#[derive(Clone, Debug, PartialEq, Eq)]
135pub enum FlowValidationError {
136    /// Flow has no nodes.
137    EmptyFlow,
138    /// Node references a component that is missing from the manifest set.
139    MissingComponent {
140        /// Offending node identifier.
141        node_id: NodeId,
142        /// Referenced component identifier.
143        component: ComponentId,
144    },
145    /// Component does not support the flow kind.
146    UnsupportedComponent {
147        /// Offending node identifier.
148        node_id: NodeId,
149        /// Referenced component identifier.
150        component: ComponentId,
151        /// Flow kind the node participates in.
152        flow_kind: FlowKind,
153    },
154}
155
156impl core::fmt::Display for FlowValidationError {
157    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
158        match self {
159            FlowValidationError::EmptyFlow => f.write_str("flows must declare at least one node"),
160            FlowValidationError::MissingComponent { node_id, component } => write!(
161                f,
162                "node `{}` references missing component `{}`",
163                node_id.as_str(),
164                component.as_str()
165            ),
166            FlowValidationError::UnsupportedComponent {
167                node_id,
168                component,
169                flow_kind,
170            } => write!(
171                f,
172                "component `{}` used by node `{}` does not support `{:?}` flows",
173                component.as_str(),
174                node_id.as_str(),
175                flow_kind
176            ),
177        }
178    }
179}
180
181#[cfg(feature = "std")]
182impl std::error::Error for FlowValidationError {}