mecha10_behavior_runtime/config/
validator.rs

1//! Validation for behavior tree configurations
2//!
3//! Ensures that behavior tree JSON is well-formed and references valid node types.
4
5use crate::registry::NodeRegistry;
6use anyhow::{Context, Result};
7use serde_json::Value;
8
9/// Validation result with detailed error information
10#[derive(Debug, Clone)]
11pub struct ValidationResult {
12    pub valid: bool,
13    pub errors: Vec<String>,
14    pub warnings: Vec<String>,
15}
16
17impl ValidationResult {
18    /// Create a successful validation result
19    pub fn success() -> Self {
20        Self {
21            valid: true,
22            errors: Vec::new(),
23            warnings: Vec::new(),
24        }
25    }
26
27    /// Create a failed validation result with errors
28    pub fn with_errors(errors: Vec<String>) -> Self {
29        Self {
30            valid: false,
31            errors,
32            warnings: Vec::new(),
33        }
34    }
35
36    /// Add a warning to the result
37    pub fn add_warning(&mut self, warning: String) {
38        self.warnings.push(warning);
39    }
40
41    /// Add an error to the result
42    pub fn add_error(&mut self, error: String) {
43        self.errors.push(error);
44        self.valid = false;
45    }
46}
47
48/// Validate a behavior tree configuration
49///
50/// Checks:
51/// - Required fields are present (name, root)
52/// - Node types exist in the registry
53/// - Configuration structure is valid
54///
55/// # Arguments
56///
57/// * `config` - Behavior tree configuration JSON
58/// * `registry` - Node registry for validating node types
59///
60/// # Example
61///
62/// ```rust,ignore
63/// use mecha10_behavior_runtime::config::validate_behavior_config;
64/// use mecha10_behavior_runtime::NodeRegistry;
65///
66/// let registry = NodeRegistry::new();
67/// let config = serde_json::json!({
68///     "name": "test_behavior",
69///     "root": {
70///         "type": "sequence",
71///         "children": []
72///     }
73/// });
74///
75/// let result = validate_behavior_config(&config, &registry)?;
76/// assert!(result.valid);
77/// ```
78pub fn validate_behavior_config(config: &Value, registry: &NodeRegistry) -> Result<ValidationResult> {
79    let mut result = ValidationResult::success();
80
81    // Check required top-level fields
82    let obj = config.as_object().context("Behavior config must be a JSON object")?;
83
84    if !obj.contains_key("name") {
85        result.add_error("Missing required field: 'name'".to_string());
86    }
87
88    if !obj.contains_key("root") {
89        result.add_error("Missing required field: 'root'".to_string());
90    } else {
91        // Validate root node
92        if let Some(root) = obj.get("root") {
93            validate_node(root, registry, &mut result, "root");
94        }
95    }
96
97    // Check for JSON schema reference (optional but recommended)
98    if !obj.contains_key("$schema") {
99        result
100            .add_warning("No '$schema' field found. Consider adding schema reference for IDE validation.".to_string());
101    }
102
103    Ok(result)
104}
105
106/// Validate a single node in the tree
107fn validate_node(node: &Value, registry: &NodeRegistry, result: &mut ValidationResult, path: &str) {
108    let obj = match node.as_object() {
109        Some(o) => o,
110        None => {
111            result.add_error(format!("Node at '{}' must be a JSON object", path));
112            return;
113        }
114    };
115
116    // Check for required 'type' field
117    let node_type = match obj.get("type").and_then(|v| v.as_str()) {
118        Some(t) => t,
119        None => {
120            result.add_error(format!("Node at '{}' missing required 'type' field", path));
121            return;
122        }
123    };
124
125    // Validate node type exists in registry (for leaf nodes)
126    // Composition nodes (sequence, selector, parallel) are built-in
127    let composition_types = ["sequence", "selector", "parallel"];
128    if !composition_types.contains(&node_type) && !registry.has_node(node_type) {
129        result.add_error(format!(
130            "Node at '{}' has unknown type '{}'. Register this node type or check for typos.",
131            path, node_type
132        ));
133    }
134
135    // Validate children for composition nodes
136    if composition_types.contains(&node_type) {
137        if let Some(children) = obj.get("children") {
138            if let Some(children_array) = children.as_array() {
139                if children_array.is_empty() {
140                    result.add_warning(format!(
141                        "Composition node at '{}' has no children (will immediately return success/failure)",
142                        path
143                    ));
144                }
145
146                for (i, child) in children_array.iter().enumerate() {
147                    let child_path = format!("{}.children[{}]", path, i);
148                    validate_node(child, registry, result, &child_path);
149                }
150            } else {
151                result.add_error(format!("Node at '{}' has 'children' field that is not an array", path));
152            }
153        } else {
154            result.add_error(format!("Composition node at '{}' missing 'children' field", path));
155        }
156    }
157
158    // Validate config field is an object if present
159    if let Some(config) = obj.get("config") {
160        if !config.is_object() {
161            result.add_error(format!("Node at '{}' has 'config' field that is not an object", path));
162        }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::NodeRegistry;
170    use serde_json::json;
171
172    #[test]
173    fn test_validate_missing_name() {
174        let registry = NodeRegistry::new();
175        let config = json!({
176            "root": {
177                "type": "sequence",
178                "children": []
179            }
180        });
181
182        let result = validate_behavior_config(&config, &registry).unwrap();
183        assert!(!result.valid);
184        assert!(result.errors.iter().any(|e| e.contains("name")));
185    }
186
187    #[test]
188    fn test_validate_missing_root() {
189        let registry = NodeRegistry::new();
190        let config = json!({
191            "name": "test"
192        });
193
194        let result = validate_behavior_config(&config, &registry).unwrap();
195        assert!(!result.valid);
196        assert!(result.errors.iter().any(|e| e.contains("root")));
197    }
198
199    #[test]
200    fn test_validate_valid_config() {
201        let registry = NodeRegistry::new();
202        let config = json!({
203            "name": "test",
204            "root": {
205                "type": "sequence",
206                "children": []
207            }
208        });
209
210        let result = validate_behavior_config(&config, &registry).unwrap();
211        assert!(result.valid);
212    }
213
214    #[test]
215    fn test_validate_unknown_node_type() {
216        let registry = NodeRegistry::new();
217        let config = json!({
218            "name": "test",
219            "root": {
220                "type": "unknown_type",
221                "config": {}
222            }
223        });
224
225        let result = validate_behavior_config(&config, &registry).unwrap();
226        assert!(!result.valid);
227        assert!(result.errors.iter().any(|e| e.contains("unknown_type")));
228    }
229}