mecha10-core 0.6.2

Mecha10 core runtime and foundational types
Documentation
//! Custom validation rules beyond what JSON Schema alone can express.

use crate::error::{Mecha10Error, Result};
use serde_json::Value;
use std::collections::HashMap;

// ============================================================================
// Custom Validation Rules
// ============================================================================

/// Apply custom validation rules beyond JSON Schema
pub(crate) fn validate_custom_rules(config: &Value) -> Result<()> {
    // Rule 1: Check for circular dependencies in nodes
    if let Some(nodes) = config.get("nodes") {
        validate_no_circular_dependencies(nodes)?;
    }

    // Rule 2: Check run_target references exist
    if let Some(nodes) = config.get("nodes") {
        if let Some(targets) = config.get("run_targets") {
            validate_run_target_references(nodes, targets)?;
        }
    }

    // Rule 3: Check node names are unique across all categories
    if let Some(nodes) = config.get("nodes") {
        validate_unique_node_names(nodes)?;
    }

    // Rule 4: Validate resource specifications
    if let Some(targets) = config.get("run_targets") {
        validate_resource_specs(targets)?;
    }

    Ok(())
}

/// Check for circular dependencies in node definitions
fn validate_no_circular_dependencies(nodes: &Value) -> Result<()> {
    let mut node_deps: HashMap<String, Vec<String>> = HashMap::new();

    // Collect all dependencies
    for category in ["drivers", "standard", "custom"] {
        if let Some(node_list) = nodes.get(category).and_then(|v| v.as_array()) {
            for node in node_list {
                if let Some(name) = node.get("name").and_then(|n| n.as_str()) {
                    let deps = node
                        .get("depends_on")
                        .and_then(|d| d.as_array())
                        .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
                        .unwrap_or_default();

                    node_deps.insert(name.to_string(), deps);
                }
            }
        }
    }

    // Check for cycles using DFS
    for node in node_deps.keys() {
        let mut visited = std::collections::HashSet::new();
        let mut stack = std::collections::HashSet::new();

        if has_cycle(node, &node_deps, &mut visited, &mut stack) {
            return Err(Mecha10Error::Configuration(format!(
                "Circular dependency detected involving node '{}'",
                node
            )));
        }
    }

    Ok(())
}

fn has_cycle(
    node: &str,
    deps: &HashMap<String, Vec<String>>,
    visited: &mut std::collections::HashSet<String>,
    stack: &mut std::collections::HashSet<String>,
) -> bool {
    if stack.contains(node) {
        return true; // Cycle detected
    }

    if visited.contains(node) {
        return false; // Already checked
    }

    visited.insert(node.to_string());
    stack.insert(node.to_string());

    if let Some(dependencies) = deps.get(node) {
        for dep in dependencies {
            if has_cycle(dep, deps, visited, stack) {
                return true;
            }
        }
    }

    stack.remove(node);
    false
}

/// Validate that all run_target references exist
fn validate_run_target_references(nodes: &Value, targets: &Value) -> Result<()> {
    let target_names: Vec<String> = targets
        .as_object()
        .map(|obj| obj.keys().cloned().collect())
        .unwrap_or_default();

    for category in ["drivers", "custom"] {
        if let Some(node_list) = nodes.get(category).and_then(|v| v.as_array()) {
            for node in node_list {
                if let Some(run_target) = node.get("run_target").and_then(|t| t.as_str()) {
                    if !target_names.contains(&run_target.to_string()) {
                        let node_name = node.get("name").and_then(|n| n.as_str()).unwrap_or("<unnamed>");

                        return Err(Mecha10Error::Configuration(format!(
                            "Node '{}' references undefined run_target '{}'. Available targets: {}",
                            node_name,
                            run_target,
                            target_names.join(", ")
                        )));
                    }
                }
            }
        }
    }

    Ok(())
}

/// Validate that node names are unique
fn validate_unique_node_names(nodes: &Value) -> Result<()> {
    let mut seen_names = std::collections::HashSet::new();

    for category in ["drivers", "standard", "custom"] {
        if let Some(node_list) = nodes.get(category).and_then(|v| v.as_array()) {
            for node in node_list {
                if let Some(name) = node.get("name").and_then(|n| n.as_str()) {
                    if !seen_names.insert(name.to_string()) {
                        return Err(Mecha10Error::Configuration(format!(
                            "Duplicate node name '{}' found. Node names must be unique across all categories.",
                            name
                        )));
                    }
                }
            }
        }
    }

    Ok(())
}

/// Validate resource specifications
fn validate_resource_specs(targets: &Value) -> Result<()> {
    if let Some(targets_obj) = targets.as_object() {
        for (target_name, target_config) in targets_obj {
            if let Some(resources) = target_config.get("resources") {
                // Validate memory format
                if let Some(mem_limit) = resources.get("memory_limit").and_then(|m| m.as_str()) {
                    if !is_valid_memory_spec(mem_limit) {
                        return Err(Mecha10Error::Configuration(format!(
                            "Invalid memory_limit '{}' in run_target '{}'. Expected format: <number><K|M|G|T>",
                            mem_limit, target_name
                        )));
                    }
                }

                // Validate CPU format
                if let Some(cpu_limit) = resources.get("cpu_limit").and_then(|c| c.as_str()) {
                    if cpu_limit.parse::<f64>().is_err() {
                        return Err(Mecha10Error::Configuration(format!(
                            "Invalid cpu_limit '{}' in run_target '{}'. Expected a number (e.g., '2.0')",
                            cpu_limit, target_name
                        )));
                    }
                }
            }
        }
    }

    Ok(())
}

fn is_valid_memory_spec(spec: &str) -> bool {
    let re = regex::Regex::new(r"^\d+(K|M|G|T)?$").unwrap();
    re.is_match(spec)
}