mecha10-core 0.6.2

Mecha10 core runtime and foundational types
Documentation
//! Validation functions for project and node configurations.

use super::rules::validate_custom_rules;
use super::schemas::{get_node_config_schema, get_project_schema};
use crate::error::{Mecha10Error, Result};
use anyhow::Context as _;
use jsonschema::{Draft, ValidationError};
use serde_json::Value;
use std::fs;
use std::path::Path;
use tracing::{debug, info};

// ============================================================================
// Validation Functions
// ============================================================================

/// Validate a JSON value against the project schema
///
/// # Arguments
///
/// * `config` - The configuration to validate (as serde_json::Value)
///
/// # Returns
///
/// Ok(()) if valid, Err with detailed validation errors if invalid
///
/// # Example
///
/// ```rust
/// use mecha10::schema_validation::validate_project_json;
///
/// let config = serde_json::json!({
///     "name": "my-robot",
///     "version": "1.0.0"
/// });
///
/// validate_project_json(&config)?;
/// ```
pub fn validate_project_json(config: &Value) -> Result<()> {
    validate_json_against_schema(
        config,
        &get_project_schema(),
        "Project configuration",
        "Configuration validation failed",
    )
}

/// Validate a project configuration file
///
/// # Arguments
///
/// * `path` - Path to mecha10.json file
///
/// # Returns
///
/// Ok(()) if valid, Err with detailed validation errors
///
/// # Example
///
/// ```rust
/// use mecha10::schema_validation::validate_project_config;
///
/// validate_project_config("mecha10.json")?;
/// ```
pub fn validate_project_config<P: AsRef<Path>>(path: P) -> Result<()> {
    let path = path.as_ref();

    info!("Validating project configuration: {}", path.display());

    // Read and parse file
    let content = fs::read_to_string(path)
        .with_context(|| format!("Failed to read config file: {}", path.display()))
        .map_err(|e| Mecha10Error::Configuration(format!("{:#}", e)))?;

    let config: Value = serde_json::from_str(&content)
        .with_context(|| format!("Failed to parse JSON from: {}", path.display()))
        .map_err(|e| Mecha10Error::Configuration(format!("{:#}", e)))?;

    // Validate against schema
    validate_project_json(&config)?;

    // Additional custom validations
    validate_custom_rules(&config)?;

    info!("Configuration validation passed");
    Ok(())
}

/// Validate NodeConfig JSON against schema
pub fn validate_node_config_json(config: &Value) -> Result<()> {
    validate_json_against_schema(
        config,
        &get_node_config_schema(),
        "Node configuration",
        "Node configuration validation failed",
    )
}

// ============================================================================
// Helper Functions
// ============================================================================

/// Validate a JSON value against a compiled schema, formatting any failures
/// into a single combined error message.
///
/// * `success_label` - describes the config kind in the debug log on success
/// * `failure_prefix` - prefixes the joined validation error messages on failure
fn validate_json_against_schema(
    config: &Value,
    schema: &Value,
    success_label: &str,
    failure_prefix: &str,
) -> Result<()> {
    let validator = jsonschema::options()
        .with_draft(Draft::Draft7)
        .build(schema)
        .map_err(|e| Mecha10Error::Configuration(format!("Invalid schema: {}", e)))?;

    if validator.is_valid(config) {
        debug!("{} validation passed", success_label);
        Ok(())
    } else {
        let error_messages: Vec<String> = validator
            .iter_errors(config)
            .map(|e| format_validation_error(&e))
            .collect();

        Err(Mecha10Error::Configuration(format!(
            "{}:\n{}",
            failure_prefix,
            error_messages.join("\n")
        )))
    }
}

/// Format a validation error into a human-readable message
fn format_validation_error(error: &ValidationError) -> String {
    let path_str = error.instance_path().to_string();
    let path = if path_str.is_empty() || path_str == "/" {
        "root".to_string()
    } else {
        path_str
    };

    format!("  - {}: {} ({})", path, error, error.instance())
}