jsoncompat 0.3.1

JSON Schema Compatibility Checker
Documentation
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use json_schema_ast::{JSONSchema, SchemaDocument, SchemaNode, compile};
use serde_json::Value;
use std::fs;
use std::hint::black_box;
use std::path::{Path, PathBuf};
use std::time::Duration;

const FIXTURE_ROOT: &str = "benches/fixtures";

fn bench_compile(c: &mut Criterion) {
    let fixtures = load_bench_fixtures();
    let mut group = c.benchmark_group("compile");

    for fixture in &fixtures {
        group.bench_with_input(
            BenchmarkId::from_parameter(&fixture.name),
            &fixture.schema,
            |b, schema| {
                b.iter(|| {
                    black_box(compile(black_box(schema)).unwrap_or_else(|error| {
                        panic!("compile failed for {}: {error}", fixture.name)
                    }))
                });
            },
        );
    }

    group.finish();
}

fn bench_schema_document_root(c: &mut Criterion) {
    let fixtures = load_bench_fixtures();
    let mut group = c.benchmark_group("SchemaDocument::root");

    for fixture in &fixtures {
        group.bench_with_input(
            BenchmarkId::from_parameter(&fixture.name),
            &fixture.schema,
            |b, schema| {
                b.iter(|| {
                    let document =
                        SchemaDocument::from_json(black_box(schema)).unwrap_or_else(|error| {
                            panic!(
                                "SchemaDocument::from_json failed for {}: {error}",
                                fixture.name
                            )
                        });
                    let root = document
                        .root()
                        .unwrap_or_else(|error| {
                            panic!("SchemaDocument::root failed for {}: {error}", fixture.name)
                        })
                        .clone();
                    black_box(root)
                });
            },
        );
    }

    group.finish();
}

fn bench_validate(c: &mut Criterion) {
    let fixtures = load_bench_fixtures();
    let validators = fixtures
        .iter()
        .map(|fixture| ValidationFixture {
            name: fixture.name.clone(),
            validator: compile(&fixture.schema)
                .unwrap_or_else(|error| panic!("compile failed for {}: {error}", fixture.name)),
            instance: fixture.instance.clone(),
        })
        .collect::<Vec<_>>();
    let mut group = c.benchmark_group("validate");

    for fixture in &validators {
        group.bench_with_input(
            BenchmarkId::from_parameter(&fixture.name),
            fixture,
            |b, fixture| {
                b.iter(|| black_box(fixture.validator.is_valid(black_box(&fixture.instance))));
            },
        );
    }

    group.finish();
}

fn criterion_config() -> Criterion {
    Criterion::default()
        .sample_size(20)
        .warm_up_time(Duration::from_millis(100))
        .measurement_time(Duration::from_millis(300))
}

criterion_group! {
    name = benches;
    config = criterion_config();
    targets = bench_compile, bench_schema_document_root, bench_validate,
}
criterion_main!(benches);

#[derive(Debug)]
struct BenchFixture {
    name: String,
    schema: Value,
    instance: Value,
}

struct ValidationFixture {
    name: String,
    validator: JSONSchema,
    instance: Value,
}

fn load_bench_fixtures() -> Vec<BenchFixture> {
    let mut fixture_paths = fs::read_dir(FIXTURE_ROOT)
        .unwrap_or_else(|error| panic!("failed to list {FIXTURE_ROOT}: {error}"))
        .map(|entry| {
            entry
                .unwrap_or_else(|error| panic!("failed to read {FIXTURE_ROOT} entry: {error}"))
                .path()
        })
        .filter(|path| path.extension().and_then(|extension| extension.to_str()) == Some("json"))
        .collect::<Vec<_>>();
    fixture_paths.sort();

    fixture_paths.into_iter().map(load_bench_fixture).collect()
}

fn load_bench_fixture(path: PathBuf) -> BenchFixture {
    let bytes = fs::read(&path)
        .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
    let root: Value = serde_json::from_slice(&bytes)
        .unwrap_or_else(|error| panic!("failed to parse {}: {error}", path.display()));
    let Some(name) = path
        .file_stem()
        .and_then(|stem| stem.to_str())
        .map(str::to_owned)
    else {
        panic!(
            "benchmark fixture path has no UTF-8 file stem: {}",
            path.display()
        );
    };
    let Some(schema) = root.get("schema").cloned() else {
        panic!("benchmark fixture {name} is missing a schema field");
    };
    let Some(instance) = root.get("instance").cloned() else {
        panic!("benchmark fixture {name} is missing an instance field");
    };

    assert_fixture_is_valid(&name, &schema, &instance, &path);

    BenchFixture {
        name,
        schema,
        instance,
    }
}

fn assert_fixture_is_valid(name: &str, schema: &Value, instance: &Value, path: &Path) {
    let document = SchemaDocument::from_json(schema).unwrap_or_else(|error| {
        panic!(
            "benchmark fixture {name} failed schema construction from {}: {error}",
            path.display()
        )
    });
    let ast: SchemaNode = document
        .root()
        .unwrap_or_else(|error| {
            panic!(
                "benchmark fixture {name} failed AST resolution from {}: {error}",
                path.display()
            )
        })
        .clone();
    let validator = compile(schema).unwrap_or_else(|error| {
        panic!(
            "benchmark fixture {name} failed validator compilation from {}: {error}",
            path.display()
        )
    });
    assert!(
        validator.is_valid(instance),
        "benchmark fixture {name} instance is invalid under {}",
        path.display()
    );
    black_box(ast);
}