spikard-core 0.15.5

Shared transport-agnostic primitives for Spikard runtimes
Documentation
use serde_json::json;
use spikard_core::validation::SchemaValidator;
use spikard_core::validation::error_mapper::{ErrorCondition, ErrorMapper};

#[test]
fn validator_preprocesses_binary_file_objects_recursively() {
    let schema = json!({
        "type": "object",
        "required": ["file", "files", "nested"],
        "properties": {
            "file": { "type": "string", "format": "binary" },
            "files": {
                "type": "array",
                "items": { "type": "string", "format": "binary" }
            },
            "nested": {
                "type": "object",
                "properties": {
                    "inner": { "type": "string", "format": "binary" }
                }
            }
        }
    });

    let validator = SchemaValidator::new(schema).expect("validator");

    let file_object = json!({
        "filename": "a.txt",
        "size": 5,
        "content": "hello",
        "content_type": "text/plain"
    });

    let data = json!({
        "file": &file_object,
        "files": [&file_object],
        "nested": {
            "inner": &file_object,
            "other": 1
        }
    });

    validator
        .validate(&data)
        .expect("binary preprocessing should satisfy schema");
}

#[test]
fn error_mapper_covers_fallbacks_and_common_conditions() {
    let empty_schema = json!({});
    let prop = "/properties/value";

    let cases = vec![
        (
            ErrorCondition::StringTooShort { min_length: None },
            "string_too_short",
            "String is too short",
        ),
        (
            ErrorCondition::StringTooLong { max_length: None },
            "string_too_long",
            "String is too long",
        ),
        (
            ErrorCondition::GreaterThan { value: None },
            "greater_than",
            "Input should be greater than the minimum",
        ),
        (
            ErrorCondition::GreaterThanEqual { value: None },
            "greater_than_equal",
            "Input should be greater than or equal to the minimum",
        ),
        (
            ErrorCondition::LessThan { value: None },
            "less_than",
            "Input should be less than the maximum",
        ),
        (
            ErrorCondition::LessThanEqual { value: None },
            "less_than_equal",
            "Input should be less than or equal to the maximum",
        ),
        (
            ErrorCondition::Enum { values: None },
            "enum",
            "Input should be one of the allowed values",
        ),
        (
            ErrorCondition::StringPatternMismatch { pattern: None },
            "string_pattern_mismatch",
            "String does not match expected pattern",
        ),
    ];

    for (condition, expected_type, expected_msg) in cases {
        let (error_type, msg, ctx) = ErrorMapper::map_error(&condition, &empty_schema, prop, "generic");
        assert_eq!(error_type, expected_type);
        assert_eq!(msg, expected_msg);
        assert!(ctx.is_none());
    }

    let (error_type, msg, ctx) = ErrorMapper::map_error(
        &ErrorCondition::TypeMismatch {
            expected_type: "integer".to_string(),
        },
        &empty_schema,
        prop,
        "generic",
    );
    assert_eq!(error_type, "int_parsing");
    assert!(msg.contains("valid integer"));
    assert!(ctx.is_none());

    let (error_type, msg, ctx) = ErrorMapper::map_error(&ErrorCondition::Missing, &empty_schema, prop, "generic");
    assert_eq!(error_type, "missing");
    assert_eq!(msg, "Field required");
    assert!(ctx.is_none());

    let (error_type, msg, ctx) = ErrorMapper::map_error(
        &ErrorCondition::AdditionalProperties {
            field: "extra".to_string(),
        },
        &empty_schema,
        prop,
        "generic",
    );
    assert_eq!(error_type, "validation_error");
    assert_eq!(msg, "Additional properties are not allowed");
    assert_eq!(ctx.as_ref().unwrap()["unexpected_field"], "extra");

    let (error_type, msg, ctx) = ErrorMapper::map_error(
        &ErrorCondition::TooFewItems { min_items: Some(2) },
        &empty_schema,
        prop,
        "generic",
    );
    assert_eq!(error_type, "too_short");
    assert!(msg.contains("at least 2"));
    assert_eq!(ctx.as_ref().unwrap()["min_length"], 2);

    let (error_type, msg, ctx) = ErrorMapper::map_error(&ErrorCondition::TooManyItems, &empty_schema, prop, "generic");
    assert_eq!(error_type, "too_long");
    assert!(msg.contains("at most"));
    assert!(ctx.as_ref().unwrap().get("max_length").is_some());
}

#[test]
fn error_mapper_uses_schema_constraints_when_present() {
    let schema = json!({
        "type": "object",
        "properties": {
            "value": {
                "type": "string",
                "minLength": 2,
                "maxLength": 4,
                "pattern": "^a+$",
                "enum": ["a", "aa"]
            },
            "num": {
                "type": "integer",
                "exclusiveMinimum": 0,
                "minimum": 1,
                "exclusiveMaximum": 10,
                "maximum": 9
            }
        }
    });

    let (ty, _msg, ctx) = ErrorMapper::map_error(
        &ErrorCondition::StringTooShort { min_length: None },
        &schema,
        "/properties/value",
        "generic",
    );
    assert_eq!(ty, "string_too_short");
    assert_eq!(ctx.as_ref().unwrap()["min_length"], 2);

    let (ty, _msg, ctx) = ErrorMapper::map_error(
        &ErrorCondition::StringTooLong { max_length: None },
        &schema,
        "/properties/value",
        "generic",
    );
    assert_eq!(ty, "string_too_long");
    assert_eq!(ctx.as_ref().unwrap()["max_length"], 4);

    let (ty, msg, ctx) = ErrorMapper::map_error(
        &ErrorCondition::Enum { values: None },
        &schema,
        "/properties/value",
        "generic",
    );
    assert_eq!(ty, "enum");
    assert!(msg.contains("or"));
    assert!(ctx.as_ref().unwrap()["expected"].as_str().unwrap().contains("or"));

    let (ty, _msg, ctx) = ErrorMapper::map_error(
        &ErrorCondition::StringPatternMismatch { pattern: None },
        &schema,
        "/properties/value",
        "generic",
    );
    assert_eq!(ty, "string_pattern_mismatch");
    assert_eq!(ctx.as_ref().unwrap()["pattern"], "^a+$");

    let (ty, _msg, ctx) = ErrorMapper::map_error(
        &ErrorCondition::GreaterThan { value: None },
        &schema,
        "/properties/num",
        "generic",
    );
    assert_eq!(ty, "greater_than");
    assert_eq!(ctx.as_ref().unwrap()["gt"], 0);

    let (ty, _msg, ctx) = ErrorMapper::map_error(
        &ErrorCondition::GreaterThanEqual { value: None },
        &schema,
        "/properties/num",
        "generic",
    );
    assert_eq!(ty, "greater_than_equal");
    assert_eq!(ctx.as_ref().unwrap()["ge"], 1);

    let (ty, _msg, ctx) = ErrorMapper::map_error(
        &ErrorCondition::LessThan { value: None },
        &schema,
        "/properties/num",
        "generic",
    );
    assert_eq!(ty, "less_than");
    assert_eq!(ctx.as_ref().unwrap()["lt"], 10);

    let (ty, _msg, ctx) = ErrorMapper::map_error(
        &ErrorCondition::LessThanEqual { value: None },
        &schema,
        "/properties/num",
        "generic",
    );
    assert_eq!(ty, "less_than_equal");
    assert_eq!(ctx.as_ref().unwrap()["le"], 9);

    let (ty, _msg, ctx) = ErrorMapper::map_error(&ErrorCondition::EmailFormat, &schema, "/properties/value", "generic");
    assert_eq!(ty, "string_pattern_mismatch");
    assert!(ctx.as_ref().unwrap()["pattern"].as_str().unwrap().contains('@'));

    let (ty, _msg, ctx) = ErrorMapper::map_error(&ErrorCondition::UuidFormat, &schema, "/properties/value", "generic");
    assert_eq!(ty, "uuid_parsing");
    assert!(ctx.is_none());
}