mecha10_behavior_runtime/loader.rs
1//! Behavior tree loader for creating trees from JSON configuration
2//!
3//! This module provides the `BehaviorLoader` for loading behavior trees from
4//! `BehaviorConfig` definitions using a `NodeRegistry`.
5
6use crate::composition::{ParallelNode, SelectOrNode, SequenceNode};
7use crate::config::{BehaviorConfig, CompositionConfig};
8use crate::registry::NodeRegistry;
9use crate::BoxedBehavior;
10use anyhow::{anyhow, Context as AnyhowContext, Result};
11use serde_json::Value;
12use tracing::{debug, info};
13
14/// Loader for creating behavior trees from JSON configuration.
15///
16/// The `BehaviorLoader` takes a `BehaviorConfig` (which can be loaded from JSON)
17/// and uses a `NodeRegistry` to instantiate the complete behavior tree.
18///
19/// # Features
20///
21/// - Recursive composition parsing (sequence/selector/parallel nodes)
22/// - Config reference resolution from the config map
23/// - Inline config support
24/// - Clear error messages for missing node types
25///
26/// # Example
27///
28/// ```rust
29/// use mecha10_behavior_runtime::prelude::*;
30/// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
31/// use serde_json::json;
32///
33/// # #[derive(Debug)]
34/// # struct MyBehavior;
35/// # #[async_trait::async_trait]
36/// # impl BehaviorNode for MyBehavior {
37/// # async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
38/// # Ok(NodeStatus::Success)
39/// # }
40/// # }
41///
42/// // Create and populate registry
43/// let mut registry = NodeRegistry::new();
44/// registry.register("my_behavior", |_| Ok(Box::new(MyBehavior)));
45///
46/// // Load behavior tree from JSON
47/// let json = r#"{
48/// "name": "test_behavior",
49/// "root": {
50/// "type": "sequence",
51/// "children": [
52/// {
53/// "type": "node",
54/// "node": "my_behavior"
55/// }
56/// ]
57/// }
58/// }"#;
59///
60/// let config = BehaviorConfig::from_json(json).unwrap();
61/// let loader = BehaviorLoader::new(registry);
62/// let behavior = loader.load(&config).unwrap();
63/// ```
64#[derive(Clone)]
65pub struct BehaviorLoader {
66 registry: NodeRegistry,
67}
68
69impl BehaviorLoader {
70 /// Create a new behavior loader with the given node registry.
71 ///
72 /// # Arguments
73 ///
74 /// * `registry` - The node registry containing registered node types
75 ///
76 /// # Example
77 ///
78 /// ```rust
79 /// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
80 ///
81 /// let registry = NodeRegistry::new();
82 /// let loader = BehaviorLoader::new(registry);
83 /// ```
84 pub fn new(registry: NodeRegistry) -> Self {
85 Self { registry }
86 }
87
88 /// Load a behavior tree from a configuration.
89 ///
90 /// This is the main entry point for loading behavior trees. It takes a
91 /// `BehaviorConfig` (typically loaded from JSON) and recursively instantiates
92 /// all nodes in the tree.
93 ///
94 /// # Arguments
95 ///
96 /// * `config` - The behavior configuration to load
97 ///
98 /// # Returns
99 ///
100 /// A boxed behavior node representing the root of the tree, or an error if:
101 /// - A referenced node type is not registered
102 /// - A config reference is invalid
103 /// - A factory function fails
104 ///
105 /// # Example
106 ///
107 /// ```rust
108 /// use mecha10_behavior_runtime::{BehaviorConfig, NodeRegistry, BehaviorLoader};
109 ///
110 /// # let mut registry = NodeRegistry::new();
111 /// # registry.register("test", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
112 /// let loader = BehaviorLoader::new(registry);
113 ///
114 /// let json = r#"{
115 /// "name": "test",
116 /// "root": {
117 /// "type": "node",
118 /// "node": "test"
119 /// }
120 /// }"#;
121 ///
122 /// let config = BehaviorConfig::from_json(json).unwrap();
123 /// let behavior = loader.load(&config).unwrap();
124 /// ```
125 pub fn load(&self, config: &BehaviorConfig) -> Result<BoxedBehavior> {
126 info!("Loading behavior tree: {}", config.name);
127 if let Some(desc) = &config.description {
128 debug!("Description: {}", desc);
129 }
130
131 self.load_composition(&config.root, config)
132 .with_context(|| format!("Failed to load behavior tree '{}'", config.name))
133 }
134
135 /// Load a behavior tree from a JSON string.
136 ///
137 /// This is a convenience method that combines parsing and loading.
138 ///
139 /// # Arguments
140 ///
141 /// * `json` - JSON string containing a BehaviorConfig
142 ///
143 /// # Example
144 ///
145 /// ```rust
146 /// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
147 ///
148 /// # let mut registry = NodeRegistry::new();
149 /// # registry.register("test", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
150 /// let loader = BehaviorLoader::new(registry);
151 ///
152 /// let json = r#"{
153 /// "name": "test",
154 /// "root": {
155 /// "type": "node",
156 /// "node": "test"
157 /// }
158 /// }"#;
159 ///
160 /// let behavior = loader.load_from_json(json).unwrap();
161 /// ```
162 // duplicate: false positive — `load_from_json`/`load_from_file` are already the thinnest
163 // possible named wrappers around the shared `self.load(...)` logic; they differ in the
164 // `BehaviorConfig` constructor they delegate to (parse a JSON string vs. read a file path),
165 // which is the actual behavior each name promises, not incidental drift to squeeze out.
166 pub fn load_from_json(&self, json: &str) -> Result<BoxedBehavior> {
167 self.load(&BehaviorConfig::from_json(json)?)
168 }
169
170 /// Load a behavior tree from a JSON file.
171 ///
172 /// This is a convenience method that reads a file and loads the behavior tree.
173 ///
174 /// # Arguments
175 ///
176 /// * `path` - Path to the JSON file
177 ///
178 /// # Example
179 ///
180 /// ```rust,no_run
181 /// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
182 ///
183 /// # let registry = NodeRegistry::new();
184 /// let loader = BehaviorLoader::new(registry);
185 /// let behavior = loader.load_from_file("configs/behaviors/@mecha10/patrol/config.json").unwrap();
186 /// ```
187 pub fn load_from_file(&self, path: impl AsRef<std::path::Path>) -> Result<BoxedBehavior> {
188 self.load(&BehaviorConfig::from_file(path)?)
189 }
190
191 /// Recursively load a composition node and its children.
192 fn load_composition(&self, composition: &CompositionConfig, config: &BehaviorConfig) -> Result<BoxedBehavior> {
193 match composition {
194 CompositionConfig::Sequence { children, name } => {
195 debug!(
196 "Loading sequence node{}",
197 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
198 );
199
200 let child_nodes = children
201 .iter()
202 .enumerate()
203 .map(|(i, child)| {
204 self.load_composition(child, config)
205 .with_context(|| format!("Failed to load sequence child {}", i))
206 })
207 .collect::<Result<Vec<_>>>()?;
208
209 debug!("Sequence node loaded with {} children", child_nodes.len());
210 Ok(Box::new(SequenceNode::new(child_nodes)))
211 }
212
213 CompositionConfig::Selector { children, name } => {
214 debug!(
215 "Loading selector node{}",
216 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
217 );
218
219 let child_nodes = children
220 .iter()
221 .enumerate()
222 .map(|(i, child)| {
223 self.load_composition(child, config)
224 .with_context(|| format!("Failed to load selector child {}", i))
225 })
226 .collect::<Result<Vec<_>>>()?;
227
228 debug!("Selector node loaded with {} children", child_nodes.len());
229 Ok(Box::new(SelectOrNode::new(child_nodes)))
230 }
231
232 CompositionConfig::Parallel { children, policy, name } => {
233 debug!(
234 "Loading parallel node{} with policy {:?}",
235 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
236 policy
237 );
238
239 let child_nodes = children
240 .iter()
241 .enumerate()
242 .map(|(i, child)| {
243 self.load_composition(child, config)
244 .with_context(|| format!("Failed to load parallel child {}", i))
245 })
246 .collect::<Result<Vec<_>>>()?;
247
248 debug!("Parallel node loaded with {} children", child_nodes.len());
249 Ok(Box::new(ParallelNode::new(child_nodes, policy.clone().into())))
250 }
251
252 CompositionConfig::Node {
253 node,
254 config_ref,
255 config: inline_config,
256 name,
257 } => {
258 debug!(
259 "Loading leaf node '{}' (type: {}){}",
260 name.as_ref().unwrap_or(node),
261 node,
262 config_ref
263 .as_ref()
264 .map(|r| format!(" with config_ref '{}'", r))
265 .unwrap_or_default()
266 );
267
268 let node_config = if let Some(inline) = inline_config {
269 debug!("Using inline config for node '{}'", node);
270 inline.clone()
271 } else if let Some(ref_key) = config_ref {
272 config
273 .get_config(ref_key)
274 .ok_or_else(|| {
275 anyhow!(
276 "Config reference '{}' not found in configs map for node '{}'",
277 ref_key,
278 node
279 )
280 })?
281 .clone()
282 } else {
283 debug!("No config provided for node '{}'", node);
284 Value::Null
285 };
286
287 self.registry
288 .create(node, node_config)
289 .with_context(|| format!("Failed to create node '{}'", node))
290 }
291 }
292 }
293
294 /// Get a reference to the underlying node registry.
295 ///
296 /// This can be useful for inspecting registered types or adding new types
297 /// after the loader has been created.
298 pub fn registry(&self) -> &NodeRegistry {
299 &self.registry
300 }
301}
302
303// duplicate: false positive — see the note on `CircuitBreaker`'s manual `Debug` impl in
304// packages/core/src/circuit_breaker.rs.
305impl std::fmt::Debug for BehaviorLoader {
306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307 f.debug_struct("BehaviorLoader")
308 .field("registry", &self.registry)
309 .finish()
310 }
311}