Skip to main content

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}