Skip to main content

af_workflow/
registry.rs

1//! Node-type registry. Port of `platform/registry.py`.
2//!
3//! Maps a node type string (`transform.state_append`) to a factory that builds
4//! a [`StepNode`] from its config. Products extend the registry with their own
5//! business node types without touching this crate.
6
7use std::collections::HashMap;
8
9use serde_json::Value;
10
11use crate::node::StepNode;
12
13#[derive(Debug, thiserror::Error)]
14pub enum NodeError {
15    #[error("unknown node type '{0}' (not registered)")]
16    UnknownType(String),
17    #[error("node '{node_type}' has invalid config: {reason}")]
18    InvalidConfig { node_type: String, reason: String },
19}
20
21/// Builds a step node from its (raw, unresolved) config value.
22pub type StepFactory = fn(config: &Value) -> Result<Box<dyn StepNode>, NodeError>;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum FieldType {
26    String,
27    Number,
28    Bool,
29    Array,
30    Object,
31    Any,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct FieldSpec {
36    pub key: String,
37    pub ty: FieldType,
38    pub required: bool,
39}
40
41impl FieldSpec {
42    pub fn required(key: impl Into<String>, ty: FieldType) -> Self {
43        Self {
44            key: key.into(),
45            ty,
46            required: true,
47        }
48    }
49
50    pub fn optional(key: impl Into<String>, ty: FieldType) -> Self {
51        Self {
52            key: key.into(),
53            ty,
54            required: false,
55        }
56    }
57}
58
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
60pub struct NodeSchema {
61    pub fields: Vec<FieldSpec>,
62}
63
64/// Registry of step-node factories. Ingress types are tracked by name only —
65/// the runner owns their execution.
66#[derive(Debug)]
67pub struct NodeRegistry {
68    steps: HashMap<String, StepFactory>,
69    ingress: std::collections::HashSet<String>,
70    fan_out: std::collections::HashSet<String>,
71    side_effect_guards: std::collections::HashSet<String>,
72    schemas: HashMap<String, NodeSchema>,
73}
74
75impl NodeRegistry {
76    /// Empty registry — no node types known.
77    pub fn empty() -> Self {
78        Self {
79            steps: HashMap::new(),
80            ingress: Default::default(),
81            fan_out: Default::default(),
82            side_effect_guards: Default::default(),
83            schemas: HashMap::new(),
84        }
85    }
86
87    /// Registry pre-loaded with the generic (non-business) node types.
88    pub fn with_builtins() -> Self {
89        let mut r = Self::empty();
90        crate::builtins::register_builtins(&mut r);
91        r
92    }
93
94    /// Register a step-node factory under `node_type`.
95    pub fn register_step(&mut self, node_type: impl Into<String>, factory: StepFactory) {
96        self.steps.insert(node_type.into(), factory);
97    }
98
99    /// Mark a node type as an ingress source.
100    pub fn register_ingress(&mut self, node_type: impl Into<String>) {
101        self.ingress.insert(node_type.into());
102    }
103
104    /// Mark a node type as fan-out-capable (its `process` may return `FanOut`).
105    /// The validator (R4') bans these upstream of `execute.*`.
106    pub fn register_fan_out(&mut self, node_type: impl Into<String>) {
107        self.fan_out.insert(node_type.into());
108    }
109
110    pub fn is_fan_out_capable(&self, node_type: &str) -> bool {
111        self.fan_out.contains(node_type)
112    }
113
114    /// Declare a product node as an authorization guard for `execute.*` side effects.
115    pub fn register_side_effect_guard(&mut self, node_type: impl Into<String>) {
116        self.side_effect_guards.insert(node_type.into());
117    }
118
119    pub fn is_side_effect_guard(&self, node_type: &str) -> bool {
120        self.side_effect_guards.contains(node_type)
121    }
122
123    pub fn is_ingress(&self, node_type: &str) -> bool {
124        self.ingress.contains(node_type) || node_type.starts_with("ingress.")
125    }
126
127    pub fn is_step(&self, node_type: &str) -> bool {
128        self.steps.contains_key(node_type)
129    }
130
131    /// Build a step node instance from a spec node's type + config.
132    pub fn build_step(
133        &self,
134        node_type: &str,
135        config: &Value,
136    ) -> Result<Box<dyn StepNode>, NodeError> {
137        let factory = self
138            .steps
139            .get(node_type)
140            .ok_or_else(|| NodeError::UnknownType(node_type.to_string()))?;
141        factory(config)
142    }
143
144    pub fn known_step_types(&self) -> impl Iterator<Item = &str> {
145        self.steps.keys().map(|s| s.as_str())
146    }
147
148    pub fn register_schema(&mut self, node_type: impl Into<String>, schema: NodeSchema) {
149        self.schemas.insert(node_type.into(), schema);
150    }
151
152    pub fn schema(&self, node_type: &str) -> Option<&NodeSchema> {
153        self.schemas.get(node_type)
154    }
155}