mecha10_behavior_runtime/config/
validator.rs1use crate::registry::NodeRegistry;
6use anyhow::{Context, Result};
7use serde_json::Value;
8
9#[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 pub fn success() -> Self {
20 Self {
21 valid: true,
22 errors: Vec::new(),
23 warnings: Vec::new(),
24 }
25 }
26
27 pub fn with_errors(errors: Vec<String>) -> Self {
29 Self {
30 valid: false,
31 errors,
32 warnings: Vec::new(),
33 }
34 }
35
36 pub fn add_warning(&mut self, warning: String) {
38 self.warnings.push(warning);
39 }
40
41 pub fn add_error(&mut self, error: String) {
43 self.errors.push(error);
44 self.valid = false;
45 }
46}
47
48pub fn validate_behavior_config(config: &Value, registry: &NodeRegistry) -> Result<ValidationResult> {
79 let mut result = ValidationResult::success();
80
81 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 if let Some(root) = obj.get("root") {
93 validate_node(root, registry, &mut result, "root");
94 }
95 }
96
97 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
106fn 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 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 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 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 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, ®istry).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, ®istry).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, ®istry).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, ®istry).unwrap();
226 assert!(!result.valid);
227 assert!(result.errors.iter().any(|e| e.contains("unknown_type")));
228 }
229}