mecha10-behavior-runtime 0.1.25

Behavior tree runtime for Mecha10 - unified AI and logic composition system
Documentation
//! Validation for behavior tree configurations
//!
//! Ensures that behavior tree JSON is well-formed and references valid node types.

use crate::registry::NodeRegistry;
use anyhow::{Context, Result};
use serde_json::Value;

/// Validation result with detailed error information
#[derive(Debug, Clone)]
pub struct ValidationResult {
    pub valid: bool,
    pub errors: Vec<String>,
    pub warnings: Vec<String>,
}

impl ValidationResult {
    /// Create a successful validation result
    pub fn success() -> Self {
        Self {
            valid: true,
            errors: Vec::new(),
            warnings: Vec::new(),
        }
    }

    /// Create a failed validation result with errors
    pub fn with_errors(errors: Vec<String>) -> Self {
        Self {
            valid: false,
            errors,
            warnings: Vec::new(),
        }
    }

    /// Add a warning to the result
    pub fn add_warning(&mut self, warning: String) {
        self.warnings.push(warning);
    }

    /// Add an error to the result
    pub fn add_error(&mut self, error: String) {
        self.errors.push(error);
        self.valid = false;
    }
}

/// Validate a behavior tree configuration
///
/// Checks:
/// - Required fields are present (name, root)
/// - Node types exist in the registry
/// - Configuration structure is valid
///
/// # Arguments
///
/// * `config` - Behavior tree configuration JSON
/// * `registry` - Node registry for validating node types
///
/// # Example
///
/// ```rust,ignore
/// use mecha10_behavior_runtime::config::validate_behavior_config;
/// use mecha10_behavior_runtime::NodeRegistry;
///
/// let registry = NodeRegistry::new();
/// let config = serde_json::json!({
///     "name": "test_behavior",
///     "root": {
///         "type": "sequence",
///         "children": []
///     }
/// });
///
/// let result = validate_behavior_config(&config, &registry)?;
/// assert!(result.valid);
/// ```
pub fn validate_behavior_config(config: &Value, registry: &NodeRegistry) -> Result<ValidationResult> {
    let mut result = ValidationResult::success();

    // Check required top-level fields
    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 {
        // Validate root node
        if let Some(root) = obj.get("root") {
            validate_node(root, registry, &mut result, "root");
        }
    }

    // Check for JSON schema reference (optional but recommended)
    if !obj.contains_key("$schema") {
        result
            .add_warning("No '$schema' field found. Consider adding schema reference for IDE validation.".to_string());
    }

    Ok(result)
}

/// Validate a single node in the tree
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;
        }
    };

    // Check for required 'type' field
    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;
        }
    };

    // Validate node type exists in registry (for leaf nodes)
    // Composition nodes (sequence, selector, parallel) are built-in
    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
        ));
    }

    // Validate children for composition nodes
    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));
        }
    }

    // Validate config field is an object if present
    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, &registry).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, &registry).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, &registry).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, &registry).unwrap();
        assert!(!result.valid);
        assert!(result.errors.iter().any(|e| e.contains("unknown_type")));
    }
}