tea-tools 0.1.0

Portable tool specifications and execution contracts for tea-rs
Documentation
use serde_json::json;
use tea_tools::{
    CompiledToolSchema, MAX_SCHEMA_ERRORS, SchemaCompilationError, SchemaValidationFailure,
};

#[test]
fn draft_2020_12_object_schema_compiles_and_validates() {
    let schema = json!({
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "$defs": {
            "path": { "type": "string", "minLength": 1 }
        },
        "properties": {
            "path": { "$ref": "#/$defs/path" },
            "count": { "type": "integer", "minimum": 1 }
        },
        "required": ["path", "count"],
        "additionalProperties": false
    });
    let compiled = CompiledToolSchema::compile(schema.clone()).unwrap();
    assert_eq!(compiled.source(), &schema);
    compiled
        .validate(&json!({"path":"notes.txt", "count": 1}))
        .unwrap();

    let errors = compiled
        .validate(&json!({"path":"", "extra":true}))
        .unwrap_err();
    assert!(!errors.errors().is_empty());
    assert!(errors.errors().len() <= MAX_SCHEMA_ERRORS);
    assert!(errors.errors().iter().all(|error| {
        !error.code().is_empty()
            && error.message().len() <= 4096
            && error.instance_path().starts_with('/')
            || error.instance_path().is_empty()
    }));
}

#[test]
fn invalid_schema_and_external_references_fail_offline() {
    assert_eq!(
        CompiledToolSchema::compile(json!({"type": 42})).unwrap_err(),
        SchemaCompilationError::InvalidSchema
    );
    assert_eq!(
        CompiledToolSchema::compile(json!({
            "type":"object",
            "properties":{"x":{"$ref":"https://example.com/schema.json"}}
        }))
        .unwrap_err(),
        SchemaCompilationError::ExternalReference
    );
    assert_eq!(
        CompiledToolSchema::compile(json!({
            "type":"object",
            "properties":{"x":{"$ref":"file:///tmp/schema.json"}}
        }))
        .unwrap_err(),
        SchemaCompilationError::ExternalReference
    );
}

#[test]
fn schema_and_instance_bounds_are_enforced() {
    let oversized = json!({"type":"object", "description":"x".repeat(256 * 1024)});
    assert_eq!(
        CompiledToolSchema::compile(oversized).unwrap_err(),
        SchemaCompilationError::SchemaOutOfBounds
    );

    let mut nested = json!({"type":"object"});
    for _ in 0..40 {
        nested = json!({"type":"object", "properties":{"next":nested}});
    }
    assert_eq!(
        CompiledToolSchema::compile(nested).unwrap_err(),
        SchemaCompilationError::SchemaOutOfBounds
    );

    let compiled = CompiledToolSchema::compile(json!({"type":"object"})).unwrap();
    assert!(matches!(
        compiled.validate(&json!({"data":"x".repeat(256 * 1024)})),
        Err(SchemaValidationFailure::ValueOutOfBounds)
    ));
}

#[test]
fn validation_errors_are_deterministically_sorted_and_capped() {
    let properties = (0..32)
        .map(|index| (format!("field_{index:02}"), json!({"type":"string"})))
        .collect::<serde_json::Map<_, _>>();
    let required = (0..32)
        .map(|index| json!(format!("field_{index:02}")))
        .collect::<Vec<_>>();
    let compiled = CompiledToolSchema::compile(json!({
        "type":"object",
        "properties":properties,
        "required":required
    }))
    .unwrap();
    let failure = compiled.validate(&json!({})).unwrap_err();
    let errors = failure.errors();
    assert_eq!(errors.len(), MAX_SCHEMA_ERRORS);
    assert!(errors.windows(2).all(|pair| {
        (
            pair[0].instance_path(),
            pair[0].schema_path(),
            pair[0].message(),
        ) <= (
            pair[1].instance_path(),
            pair[1].schema_path(),
            pair[1].message(),
        )
    }));
}