mecha10_behavior_runtime/registry.rs
1//! Node registry for dynamic behavior instantiation
2//!
3//! This module provides the `NodeRegistry` for registering and creating behavior nodes
4//! from string identifiers and JSON configuration.
5
6use crate::BoxedBehavior;
7use anyhow::{anyhow, Context as AnyhowContext, Result};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::sync::{Arc, RwLock};
11
12/// Type alias for node factory functions.
13///
14/// A factory function takes a JSON configuration value and returns a boxed behavior node.
15/// The configuration value can be used to parameterize the behavior at construction time.
16///
17/// # Example
18///
19/// ```rust
20/// use mecha10_behavior_runtime::prelude::*;
21/// use serde_json::Value;
22///
23/// fn create_my_behavior(config: Value) -> anyhow::Result<BoxedBehavior> {
24/// // Parse configuration
25/// let speed = config.get("speed")
26/// .and_then(|v| v.as_f64())
27/// .unwrap_or(1.0);
28///
29/// // Create and return behavior
30/// // Ok(Box::new(MyBehavior::new(speed)))
31/// # Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![])))
32/// }
33/// ```
34pub type NodeFactory = Arc<dyn Fn(Value) -> Result<BoxedBehavior> + Send + Sync>;
35
36/// Registry for behavior node types.
37///
38/// The `NodeRegistry` provides a factory pattern for creating behavior nodes from
39/// string identifiers and JSON configuration. This enables loading behavior trees
40/// from JSON files without hardcoding node types.
41///
42/// # Thread Safety
43///
44/// The registry is thread-safe and can be shared across multiple threads using `Arc`.
45/// Internally it uses `RwLock` to allow concurrent reads while serializing writes.
46///
47/// # Example
48///
49/// ```rust
50/// use mecha10_behavior_runtime::prelude::*;
51/// use mecha10_behavior_runtime::NodeRegistry;
52/// use serde_json::json;
53///
54/// # #[derive(Debug)]
55/// # struct MyBehavior;
56/// # #[async_trait::async_trait]
57/// # impl BehaviorNode for MyBehavior {
58/// # async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
59/// # Ok(NodeStatus::Success)
60/// # }
61/// # }
62///
63/// // Create a registry
64/// let mut registry = NodeRegistry::new();
65///
66/// // Register a node type
67/// registry.register("my_behavior", |config| {
68/// Ok(Box::new(MyBehavior))
69/// });
70///
71/// // Create an instance from the registry
72/// let node = registry.create("my_behavior", json!({})).unwrap();
73/// ```
74#[derive(Clone)]
75pub struct NodeRegistry {
76 factories: Arc<RwLock<HashMap<String, NodeFactory>>>,
77}
78
79impl NodeRegistry {
80 // duplicate: false positive — matches `CircuitBreakerRegistry::new`
81 // (packages/core/src/rpc_breaker.rs) and `LogStreamManager::new`
82 // (packages/launcher-service/src/logging.rs) on AST shape only: wrap a fresh `HashMap` in
83 // `Arc<RwLock<_>>`. The three registries track unrelated domains (behavior node factories,
84 // per-endpoint circuit breakers, per-stream log handles) and share no logic beyond this
85 // one-line initializer, so there's nothing to extract into a common helper that wouldn't
86 // just be `Arc::new(RwLock::new(HashMap::new()))` again.
87 /// Create a new empty node registry.
88 pub fn new() -> Self {
89 Self {
90 factories: Arc::new(RwLock::new(HashMap::new())),
91 }
92 }
93
94 /// Register a node type with a factory function.
95 ///
96 /// The factory function will be called each time a node of this type is created.
97 /// It receives the JSON configuration and should return a boxed behavior node.
98 ///
99 /// # Arguments
100 ///
101 /// * `node_type` - String identifier for this node type (e.g., "move_to_goal")
102 /// * `factory` - Function that creates instances of this node type
103 ///
104 /// # Example
105 ///
106 /// ```rust
107 /// use mecha10_behavior_runtime::NodeRegistry;
108 /// use serde_json::json;
109 ///
110 /// # #[derive(Debug)]
111 /// # struct MoveBehavior { speed: f64 }
112 /// # #[async_trait::async_trait]
113 /// # impl mecha10_behavior_runtime::BehaviorNode for MoveBehavior {
114 /// # async fn tick(&mut self, _ctx: &mecha10_behavior_runtime::Context) -> anyhow::Result<mecha10_behavior_runtime::NodeStatus> {
115 /// # Ok(mecha10_behavior_runtime::NodeStatus::Success)
116 /// # }
117 /// # }
118 ///
119 /// let mut registry = NodeRegistry::new();
120 ///
121 /// registry.register("move", |config| {
122 /// let speed = config.get("speed")
123 /// .and_then(|v| v.as_f64())
124 /// .unwrap_or(1.0);
125 /// Ok(Box::new(MoveBehavior { speed }))
126 /// });
127 /// ```
128 pub fn register<F>(&mut self, node_type: impl Into<String>, factory: F)
129 where
130 F: Fn(Value) -> Result<BoxedBehavior> + Send + Sync + 'static,
131 {
132 let mut factories = self.factories.write().unwrap();
133 factories.insert(node_type.into(), Arc::new(factory));
134 }
135
136 /// Create a node instance from a registered type.
137 ///
138 /// This looks up the factory function for the given node type and calls it
139 /// with the provided configuration to create a new instance.
140 ///
141 /// # Arguments
142 ///
143 /// * `node_type` - The type identifier that was used during registration
144 /// * `config` - JSON configuration to pass to the factory function
145 ///
146 /// # Returns
147 ///
148 /// A boxed behavior node instance, or an error if:
149 /// - The node type is not registered
150 /// - The factory function returns an error
151 ///
152 /// # Example
153 ///
154 /// ```rust
155 /// use mecha10_behavior_runtime::NodeRegistry;
156 /// use serde_json::json;
157 ///
158 /// # let mut registry = NodeRegistry::new();
159 /// # registry.register("move", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
160 ///
161 /// let config = json!({ "speed": 2.0 });
162 /// let node = registry.create("move", config).unwrap();
163 /// ```
164 pub fn create(&self, node_type: &str, config: Value) -> Result<BoxedBehavior> {
165 let factories = self.factories.read().unwrap();
166
167 let factory = factories
168 .get(node_type)
169 .ok_or_else(|| anyhow!("Node type '{}' not registered", node_type))?;
170
171 factory(config).with_context(|| format!("Failed to create node of type '{}'", node_type))
172 }
173
174 /// Check if a node type is registered.
175 ///
176 /// # Arguments
177 ///
178 /// * `node_type` - The type identifier to check
179 ///
180 /// # Returns
181 ///
182 /// `true` if the node type is registered, `false` otherwise
183 ///
184 /// # Example
185 ///
186 /// ```rust
187 /// use mecha10_behavior_runtime::NodeRegistry;
188 ///
189 /// let mut registry = NodeRegistry::new();
190 /// registry.register("move", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
191 ///
192 /// assert!(registry.has_node("move"));
193 /// assert!(!registry.has_node("unknown"));
194 /// ```
195 pub fn has_node(&self, node_type: &str) -> bool {
196 self.contains(node_type)
197 }
198
199 /// Check if a node type is registered.
200 ///
201 /// Same check as [`has_node`](Self::has_node) - both names are kept since callers use
202 /// either spelling, but the lookup itself lives here to avoid duplicating the `RwLock` read.
203 ///
204 /// # Example
205 ///
206 /// ```rust
207 /// use mecha10_behavior_runtime::NodeRegistry;
208 ///
209 /// let mut registry = NodeRegistry::new();
210 /// registry.register("move", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
211 ///
212 /// assert!(registry.contains("move"));
213 /// assert!(!registry.contains("unknown"));
214 /// ```
215 pub fn contains(&self, node_type: &str) -> bool {
216 let factories = self.factories.read().unwrap();
217 factories.contains_key(node_type)
218 }
219
220 /// Get a list of all registered node types.
221 ///
222 /// # Example
223 ///
224 /// ```rust
225 /// use mecha10_behavior_runtime::NodeRegistry;
226 ///
227 /// let mut registry = NodeRegistry::new();
228 /// registry.register("move", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
229 /// registry.register("rotate", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
230 ///
231 /// let types = registry.registered_types();
232 /// assert!(types.contains(&"move".to_string()));
233 /// assert!(types.contains(&"rotate".to_string()));
234 /// ```
235 pub fn registered_types(&self) -> Vec<String> {
236 let factories = self.factories.read().unwrap();
237 factories.keys().cloned().collect()
238 }
239
240 /// Get the number of registered node types.
241 ///
242 /// # Example
243 ///
244 /// ```rust
245 /// use mecha10_behavior_runtime::NodeRegistry;
246 ///
247 /// let mut registry = NodeRegistry::new();
248 /// assert_eq!(registry.len(), 0);
249 ///
250 /// registry.register("move", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
251 /// assert_eq!(registry.len(), 1);
252 /// ```
253 pub fn len(&self) -> usize {
254 let factories = self.factories.read().unwrap();
255 factories.len()
256 }
257
258 /// Check if the registry is empty.
259 ///
260 /// # Example
261 ///
262 /// ```rust
263 /// use mecha10_behavior_runtime::NodeRegistry;
264 ///
265 /// let registry = NodeRegistry::new();
266 /// assert!(registry.is_empty());
267 /// ```
268 pub fn is_empty(&self) -> bool {
269 self.len() == 0
270 }
271}
272
273impl Default for NodeRegistry {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279impl std::fmt::Debug for NodeRegistry {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 let factories = self.factories.read().unwrap();
282 f.debug_struct("NodeRegistry")
283 .field("registered_types", &factories.keys().collect::<Vec<_>>())
284 .finish()
285 }
286}