1use 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
21pub 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#[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 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 pub fn with_builtins() -> Self {
89 let mut r = Self::empty();
90 crate::builtins::register_builtins(&mut r);
91 r
92 }
93
94 pub fn register_step(&mut self, node_type: impl Into<String>, factory: StepFactory) {
96 self.steps.insert(node_type.into(), factory);
97 }
98
99 pub fn register_ingress(&mut self, node_type: impl Into<String>) {
101 self.ingress.insert(node_type.into());
102 }
103
104 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 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 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}