data-protocol-validator 0.1.0

Rust validator for Data Protocol schemas - validates versioned bioinformatics analysis output against JSON Schema-based protocol definitions
Documentation
use std::collections::HashMap;

/// Severity level for a validation error or warning.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Severity {
    Error,
    Warning,
}

impl Severity {
    pub fn as_str(&self) -> &'static str {
        match self {
            Severity::Error => "error",
            Severity::Warning => "warning",
        }
    }
}

struct ErrorRegistryEntry {
    severity: Severity,
    message_template: &'static str,
}

/// Build the static error registry mapping error codes to their severity and
/// message template.
fn error_registry() -> HashMap<&'static str, ErrorRegistryEntry> {
    let mut m = HashMap::new();
    m.insert(
        "E001",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "Type mismatch: expected {expected}, got {actual}",
        },
    );
    m.insert(
        "E002",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "Required property \"{property}\" is missing",
        },
    );
    m.insert(
        "E003",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "Additional property \"{property}\" is not allowed",
        },
    );
    m.insert(
        "E004",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "String constraint violation: {constraint}",
        },
    );
    m.insert(
        "E005",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "Number constraint violation: {constraint}",
        },
    );
    m.insert(
        "E006",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "Array constraint violation: {constraint}",
        },
    );
    m.insert(
        "E007",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "Object constraint violation: {constraint}",
        },
    );
    m.insert(
        "E008",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "Format validation failed: expected {format}, got \"{value}\"",
        },
    );
    m.insert(
        "E009",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "Value {value} does not match {constraint}",
        },
    );
    m.insert(
        "E010",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "Composition constraint \"{keyword}\" not satisfied",
        },
    );
    m.insert(
        "E011",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "Failed to resolve $ref: \"{ref}\"",
        },
    );
    m.insert(
        "E012",
        ErrorRegistryEntry {
            severity: Severity::Error,
            message_template: "Invalid protocol: {reason}",
        },
    );
    m.insert(
        "W001",
        ErrorRegistryEntry {
            severity: Severity::Warning,
            message_template: "Field \"{field}\" is deprecated: {reason}",
        },
    );
    m.insert(
        "W002",
        ErrorRegistryEntry {
            severity: Severity::Warning,
            message_template: "Unknown extension property: \"{property}\"",
        },
    );
    m
}

/// Format a message template by replacing `{key}` placeholders with values
/// from the context map.
fn format_template(template: &str, context: &HashMap<&str, String>) -> String {
    let mut result = template.to_string();
    for (key, value) in context {
        let placeholder = format!("{{{}}}", key);
        result = result.replace(&placeholder, value);
    }
    result
}

use crate::types::ValidationError;

/// Create a `ValidationError` from an error code, JSON path, and context
/// values that are interpolated into the message template.
pub fn create_error(code: &str, path: &str, context: HashMap<&str, String>) -> ValidationError {
    let registry = error_registry();
    match registry.get(code) {
        Some(entry) => ValidationError {
            code: code.to_string(),
            path: path.to_string(),
            message: format_template(entry.message_template, &context),
            severity: entry.severity.as_str().to_string(),
            suggestion: None,
        },
        None => ValidationError {
            code: code.to_string(),
            path: path.to_string(),
            message: format!("Unknown error code: {}", code),
            severity: "error".to_string(),
            suggestion: None,
        },
    }
}