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;
12use crate::{CapabilityManifest, GuardKind};
13
14/// Node construction or registry failure.
15#[derive(Debug, thiserror::Error)]
16pub enum NodeError {
17    /// Unknown node type '' (not registered).
18    #[error("unknown node type '{0}' (not registered)")]
19    UnknownType(String),
20    /// Node '`node_type`' has invalid config: `reason`.
21    #[error("node '{node_type}' has invalid config: {reason}")]
22    InvalidConfig {
23        /// Node type being built.
24        node_type: String,
25        /// What is wrong with the config.
26        reason: String,
27    },
28    /// Invalid capability manifest.
29    #[error("invalid capability manifest: {0}")]
30    InvalidCapability(String),
31    /// Capability '' is already registered.
32    #[error("capability '{0}' is already registered")]
33    DuplicateCapability(String),
34}
35
36/// Builds a step node from its (raw, unresolved) config value.
37pub type StepFactory = fn(config: &Value) -> Result<Box<dyn StepNode>, NodeError>;
38
39/// Config field type in a node schema.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum FieldType {
42    /// String.
43    String,
44    /// Number.
45    Number,
46    /// Boolean.
47    Bool,
48    /// Array.
49    Array,
50    /// Object.
51    Object,
52    /// Any JSON value.
53    Any,
54}
55
56/// One config field.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct FieldSpec {
59    /// Field name.
60    pub key: String,
61    /// Expected type.
62    pub ty: FieldType,
63    /// Whether it must be present.
64    pub required: bool,
65}
66
67impl FieldSpec {
68    /// A required field.
69    pub fn required(key: impl Into<String>, ty: FieldType) -> Self {
70        Self {
71            key: key.into(),
72            ty,
73            required: true,
74        }
75    }
76
77    /// An optional field.
78    pub fn optional(key: impl Into<String>, ty: FieldType) -> Self {
79        Self {
80            key: key.into(),
81            ty,
82            required: false,
83        }
84    }
85}
86
87/// Config schema of a node type.
88#[derive(Debug, Clone, Default, PartialEq, Eq)]
89pub struct NodeSchema {
90    /// Fields in declaration order.
91    pub fields: Vec<FieldSpec>,
92}
93
94/// Registry of step-node factories. Ingress types are tracked by name only —
95/// the runner owns their execution.
96#[derive(Debug)]
97pub struct NodeRegistry {
98    steps: HashMap<String, StepFactory>,
99    ingress: std::collections::HashSet<String>,
100    fan_out: std::collections::HashSet<String>,
101    side_effect_guards: std::collections::HashSet<String>,
102    guard_kinds: HashMap<String, GuardKind>,
103    capabilities: HashMap<String, CapabilityManifest>,
104    schemas: HashMap<String, NodeSchema>,
105}
106
107impl NodeRegistry {
108    /// Empty registry — no node types known.
109    pub fn empty() -> Self {
110        Self {
111            steps: HashMap::new(),
112            ingress: Default::default(),
113            fan_out: Default::default(),
114            side_effect_guards: Default::default(),
115            guard_kinds: Default::default(),
116            capabilities: Default::default(),
117            schemas: HashMap::new(),
118        }
119    }
120
121    /// Registry pre-loaded with the generic (non-business) node types.
122    pub fn with_builtins() -> Self {
123        let mut r = Self::empty();
124        crate::builtins::register_builtins(&mut r);
125        r
126    }
127
128    /// Register a step-node factory under `node_type`.
129    pub fn register_step(&mut self, node_type: impl Into<String>, factory: StepFactory) {
130        self.steps.insert(node_type.into(), factory);
131    }
132
133    /// Mark a node type as an ingress source.
134    pub fn register_ingress(&mut self, node_type: impl Into<String>) {
135        self.ingress.insert(node_type.into());
136    }
137
138    /// Mark a node type as fan-out-capable (its `process` may return `FanOut`).
139    /// The validator (R4') bans these upstream of `execute.*`.
140    pub fn register_fan_out(&mut self, node_type: impl Into<String>) {
141        self.fan_out.insert(node_type.into());
142    }
143
144    /// Whether the node type may return `FanOut`.
145    pub fn is_fan_out_capable(&self, node_type: &str) -> bool {
146        self.fan_out.contains(node_type)
147    }
148
149    /// Declare a product node as an authorization guard for `execute.*` side effects.
150    pub fn register_side_effect_guard(&mut self, node_type: impl Into<String>) {
151        self.register_guard(node_type, GuardKind::Authorization);
152    }
153
154    /// Declare the role a guard plays on an action's dominating path.
155    pub fn register_guard(&mut self, node_type: impl Into<String>, kind: GuardKind) {
156        let node_type = node_type.into();
157        self.side_effect_guards.insert(node_type.clone());
158        self.guard_kinds.insert(node_type, kind);
159    }
160
161    /// Guard role of a node type, if registered as a guard.
162    pub fn guard_kind(&self, node_type: &str) -> Option<GuardKind> {
163        self.guard_kinds.get(node_type).copied()
164    }
165
166    /// Whether the node type is a registered guard.
167    pub fn is_side_effect_guard(&self, node_type: &str) -> bool {
168        self.side_effect_guards.contains(node_type)
169    }
170
171    /// Whether the node type is an ingress source.
172    pub fn is_ingress(&self, node_type: &str) -> bool {
173        self.ingress.contains(node_type) || node_type.starts_with("ingress.")
174    }
175
176    /// Whether the node type can be compiled as a step.
177    pub fn is_step(&self, node_type: &str) -> bool {
178        self.steps.contains_key(node_type)
179    }
180
181    /// Build a step node instance from a spec node's type + config.
182    pub fn build_step(
183        &self,
184        node_type: &str,
185        config: &Value,
186    ) -> Result<Box<dyn StepNode>, NodeError> {
187        let factory = self
188            .steps
189            .get(node_type)
190            .ok_or_else(|| NodeError::UnknownType(node_type.to_string()))?;
191        factory(config)
192    }
193
194    /// Registered step node types.
195    pub fn known_step_types(&self) -> impl Iterator<Item = &str> {
196        self.steps.keys().map(|s| s.as_str())
197    }
198
199    /// Attach a config schema to a node type.
200    pub fn register_schema(&mut self, node_type: impl Into<String>, schema: NodeSchema) {
201        self.schemas.insert(node_type.into(), schema);
202    }
203
204    /// Config schema of a node type.
205    pub fn schema(&self, node_type: &str) -> Option<&NodeSchema> {
206        self.schemas.get(node_type)
207    }
208
209    /// Register the immutable contract for a trigger, expression, guard or action.
210    pub fn register_capability(&mut self, manifest: CapabilityManifest) -> Result<(), NodeError> {
211        manifest
212            .validate()
213            .map_err(|error| NodeError::InvalidCapability(error.to_string()))?;
214        if self.capabilities.contains_key(&manifest.id) {
215            return Err(NodeError::DuplicateCapability(manifest.id));
216        }
217        self.capabilities.insert(manifest.id.clone(), manifest);
218        Ok(())
219    }
220
221    /// Registered manifest by capability id.
222    pub fn capability(&self, id: &str) -> Option<&CapabilityManifest> {
223        self.capabilities.get(id)
224    }
225
226    /// Every registered manifest.
227    pub fn capability_manifests(&self) -> impl Iterator<Item = &CapabilityManifest> {
228        self.capabilities.values()
229    }
230}