Skip to main content

bnto_core/
registry.rs

1// Node registry — maps node type keys (e.g., "image-compress") to NodeProcessor
2// instances. Decouples the executor from specific node types; consumers
3// register only the processors they need (all 6 for WASM, subset for CLI/tests).
4
5use crate::metadata::NodeMetadata;
6use crate::processor::NodeProcessor;
7use std::collections::HashMap;
8
9// =============================================================================
10// Node Registry
11// =============================================================================
12
13/// A registry that maps node type keys (e.g., "image-compress") to node processors.
14///
15/// Uses `Box<dyn NodeProcessor>` for dynamic dispatch -- the registry
16/// holds heterogeneous processor types behind a single trait object.
17pub struct NodeRegistry {
18    /// Maps node type keys (e.g., "image-compress") to processor instances.
19    processors: HashMap<String, Box<dyn NodeProcessor>>,
20}
21
22impl NodeRegistry {
23    /// Create a new empty registry.
24    pub fn new() -> Self {
25        Self {
26            processors: HashMap::new(),
27        }
28    }
29
30    /// Register a processor under a node type key.
31    ///
32    /// The key should be the node type name
33    /// (e.g., "image-compress", "spreadsheet-clean").
34    ///
35    /// If a processor is already registered under this key, it will
36    /// be replaced (last registration wins).
37    ///
38    /// # Arguments
39    /// - `key` — the node type key (e.g., "image-compress")
40    /// - `processor` — the processor instance to register
41    pub fn register(&mut self, key: &str, processor: Box<dyn NodeProcessor>) {
42        self.processors.insert(key.to_string(), processor);
43    }
44
45    /// Look up the processor for a given node type.
46    ///
47    /// Does a direct lookup by `node_type` key (e.g., "image-compress").
48    ///
49    /// # Arguments
50    /// - `node_type` — the node's type field (e.g., "image-compress", "spreadsheet-clean")
51    /// - `_params` — the node's params map (reserved for future use)
52    ///
53    /// # Returns
54    /// - `Some(&dyn NodeProcessor)` — if a matching processor was found
55    /// - `None` — if no processor matches the key
56    pub fn resolve(
57        &self,
58        node_type: &str,
59        _params: &serde_json::Map<String, serde_json::Value>,
60    ) -> Option<&dyn NodeProcessor> {
61        self.processors.get(node_type).map(|b| b.as_ref())
62    }
63
64    /// Check how many processors are registered (useful for tests).
65    pub fn len(&self) -> usize {
66        self.processors.len()
67    }
68
69    /// Check if the registry is empty.
70    pub fn is_empty(&self) -> bool {
71        self.processors.is_empty()
72    }
73
74    /// Collect metadata from ALL registered processors into a catalog.
75    ///
76    /// This returns a `Vec<NodeMetadata>` — one entry per registered processor.
77    /// The order is determined by the HashMap's iteration order (not guaranteed,
78    /// but deterministic for a given build). The `node_catalog()` WASM function
79    /// sorts the output by node type for stable snapshots.
80    pub fn catalog(&self) -> Vec<NodeMetadata> {
81        self.processors.values().map(|p| p.metadata()).collect()
82    }
83}
84
85impl Default for NodeRegistry {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91// =============================================================================
92// Tests
93// =============================================================================
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::context::ProcessContext;
99    use crate::errors::BntoError;
100    use crate::processor::{NodeInput, NodeOutput, OutputFile};
101    use crate::progress::ProgressReporter;
102
103    // --- Mock Processor for Testing ---
104
105    /// A simple mock processor that just echoes input back.
106    struct MockProcessor {
107        /// A name to identify this mock in tests.
108        mock_name: String,
109    }
110
111    impl MockProcessor {
112        fn new(name: &str) -> Self {
113            Self {
114                mock_name: name.to_string(),
115            }
116        }
117    }
118
119    impl NodeProcessor for MockProcessor {
120        fn name(&self) -> &str {
121            &self.mock_name
122        }
123
124        fn process(
125            &self,
126            input: NodeInput,
127            _progress: &ProgressReporter,
128            _ctx: &dyn ProcessContext,
129        ) -> Result<NodeOutput, BntoError> {
130            Ok(NodeOutput {
131                files: vec![OutputFile {
132                    data: input.data,
133                    filename: input.filename,
134                    mime_type: input
135                        .mime_type
136                        .unwrap_or_else(|| "application/octet-stream".to_string()),
137                }],
138                metadata: serde_json::Map::new(),
139            })
140        }
141    }
142
143    // --- Tests ---
144
145    #[test]
146    fn test_new_registry_is_empty() {
147        let registry = NodeRegistry::new();
148        assert!(registry.is_empty());
149        assert_eq!(registry.len(), 0);
150    }
151
152    #[test]
153    fn test_register_and_resolve() {
154        let mut registry = NodeRegistry::new();
155        registry.register("image-compress", Box::new(MockProcessor::new("compress")));
156
157        let params = serde_json::Map::new();
158
159        // Resolve should find the processor by node type key.
160        let processor = registry.resolve("image-compress", &params);
161        assert!(processor.is_some());
162        assert_eq!(processor.unwrap().name(), "compress");
163    }
164
165    #[test]
166    fn test_resolve_unknown_type_returns_none() {
167        let registry = NodeRegistry::new();
168        let params = serde_json::Map::new();
169
170        // Empty registry → None.
171        let processor = registry.resolve("unknown-type", &params);
172        assert!(processor.is_none());
173    }
174
175    #[test]
176    fn test_register_multiple_processors() {
177        let mut registry = NodeRegistry::new();
178        registry.register("image-compress", Box::new(MockProcessor::new("compress")));
179        registry.register("image-resize", Box::new(MockProcessor::new("resize")));
180        registry.register(
181            "spreadsheet-clean",
182            Box::new(MockProcessor::new("clean-csv")),
183        );
184
185        assert_eq!(registry.len(), 3);
186
187        let params = serde_json::Map::new();
188
189        // Each resolves to the correct processor by type key.
190        assert_eq!(
191            registry.resolve("image-compress", &params).unwrap().name(),
192            "compress"
193        );
194        assert_eq!(
195            registry.resolve("image-resize", &params).unwrap().name(),
196            "resize"
197        );
198        assert_eq!(
199            registry
200                .resolve("spreadsheet-clean", &params)
201                .unwrap()
202                .name(),
203            "clean-csv"
204        );
205    }
206
207    #[test]
208    fn test_register_overwrites_existing() {
209        let mut registry = NodeRegistry::new();
210        registry.register("image-compress", Box::new(MockProcessor::new("old")));
211        registry.register("image-compress", Box::new(MockProcessor::new("new")));
212
213        let params = serde_json::Map::new();
214
215        // Last registration wins.
216        assert_eq!(
217            registry.resolve("image-compress", &params).unwrap().name(),
218            "new"
219        );
220    }
221}