use crate::registry::NodeRegistry;
use anyhow::{Context, Result};
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl ValidationResult {
pub fn success() -> Self {
Self {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn with_errors(errors: Vec<String>) -> Self {
Self {
valid: false,
errors,
warnings: Vec::new(),
}
}
pub fn add_warning(&mut self, warning: String) {
self.warnings.push(warning);
}
pub fn add_error(&mut self, error: String) {
self.errors.push(error);
self.valid = false;
}
}
pub fn validate_behavior_config(config: &Value, registry: &NodeRegistry) -> Result<ValidationResult> {
let mut result = ValidationResult::success();
let obj = config.as_object().context("Behavior config must be a JSON object")?;
if !obj.contains_key("name") {
result.add_error("Missing required field: 'name'".to_string());
}
if !obj.contains_key("root") {
result.add_error("Missing required field: 'root'".to_string());
} else {
if let Some(root) = obj.get("root") {
validate_node(root, registry, &mut result, "root");
}
}
if !obj.contains_key("$schema") {
result
.add_warning("No '$schema' field found. Consider adding schema reference for IDE validation.".to_string());
}
Ok(result)
}
fn validate_node(node: &Value, registry: &NodeRegistry, result: &mut ValidationResult, path: &str) {
let obj = match node.as_object() {
Some(o) => o,
None => {
result.add_error(format!("Node at '{}' must be a JSON object", path));
return;
}
};
let node_type = match obj.get("type").and_then(|v| v.as_str()) {
Some(t) => t,
None => {
result.add_error(format!("Node at '{}' missing required 'type' field", path));
return;
}
};
let composition_types = ["sequence", "selector", "parallel"];
if !composition_types.contains(&node_type) && !registry.has_node(node_type) {
result.add_error(format!(
"Node at '{}' has unknown type '{}'. Register this node type or check for typos.",
path, node_type
));
}
if composition_types.contains(&node_type) {
if let Some(children) = obj.get("children") {
if let Some(children_array) = children.as_array() {
if children_array.is_empty() {
result.add_warning(format!(
"Composition node at '{}' has no children (will immediately return success/failure)",
path
));
}
for (i, child) in children_array.iter().enumerate() {
let child_path = format!("{}.children[{}]", path, i);
validate_node(child, registry, result, &child_path);
}
} else {
result.add_error(format!("Node at '{}' has 'children' field that is not an array", path));
}
} else {
result.add_error(format!("Composition node at '{}' missing 'children' field", path));
}
}
if let Some(config) = obj.get("config") {
if !config.is_object() {
result.add_error(format!("Node at '{}' has 'config' field that is not an object", path));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::NodeRegistry;
use serde_json::json;
#[test]
fn test_validate_missing_name() {
let registry = NodeRegistry::new();
let config = json!({
"root": {
"type": "sequence",
"children": []
}
});
let result = validate_behavior_config(&config, ®istry).unwrap();
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.contains("name")));
}
#[test]
fn test_validate_missing_root() {
let registry = NodeRegistry::new();
let config = json!({
"name": "test"
});
let result = validate_behavior_config(&config, ®istry).unwrap();
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.contains("root")));
}
#[test]
fn test_validate_valid_config() {
let registry = NodeRegistry::new();
let config = json!({
"name": "test",
"root": {
"type": "sequence",
"children": []
}
});
let result = validate_behavior_config(&config, ®istry).unwrap();
assert!(result.valid);
}
#[test]
fn test_validate_unknown_node_type() {
let registry = NodeRegistry::new();
let config = json!({
"name": "test",
"root": {
"type": "unknown_type",
"config": {}
}
});
let result = validate_behavior_config(&config, ®istry).unwrap();
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.contains("unknown_type")));
}
}