tea-protocol 0.1.0

Canonical protocol types for the tea-rs project
Documentation
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};

use serde_json::Value;

const SUPPORTED_FIXTURE_SETS: [(&str, &str); 2] = [("v1.0", "1.0"), ("v1.1", "1.1")];

fn fixture_dir(version: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("tests/fixtures/{version}"))
}

#[test]
fn manifest_lists_valid_json_fixtures() {
    for (version_dir, protocol_version) in SUPPORTED_FIXTURE_SETS {
        let fixture_dir = fixture_dir(version_dir);
        let manifest: Value = serde_json::from_str(
            &fs::read_to_string(fixture_dir.join("manifest.json")).expect("read fixture manifest"),
        )
        .expect("parse fixture manifest");

        assert_eq!(manifest["protocolVersion"], protocol_version);
        let fixtures = manifest["fixtures"]
            .as_array()
            .expect("fixtures must be an array");
        assert!(!fixtures.is_empty(), "fixture manifest must not be empty");

        let mut listed = BTreeSet::new();
        for fixture in fixtures {
            let category = fixture["category"]
                .as_str()
                .expect("fixture category must be a string");
            assert!(
                matches!(category, "command" | "event" | "record" | "error"),
                "unsupported fixture category: {category}"
            );

            let file = fixture["file"]
                .as_str()
                .expect("fixture file must be a string");
            assert!(
                listed.insert(file.to_owned()),
                "duplicate fixture entry: {file}"
            );
            assert!(
                file.starts_with(&format!("{category}-")),
                "fixture category/name mismatch: {file}"
            );
            let contents = fs::read_to_string(fixture_dir.join(file)).expect("read listed fixture");
            let value: Value = serde_json::from_str(&contents).expect("parse listed fixture");
            assert_eq!(value["protocolVersion"], protocol_version, "fixture {file}");
            assert!(value["type"].is_string(), "fixture {file} needs a type");
        }

        let files = fs::read_dir(&fixture_dir)
            .expect("read fixture directory")
            .map(|entry| entry.expect("read fixture entry").file_name())
            .filter_map(|name| name.to_str().map(str::to_owned))
            .filter(|name| {
                Path::new(name)
                    .extension()
                    .is_some_and(|extension| extension.eq_ignore_ascii_case("json"))
                    && name != "manifest.json"
            })
            .collect::<BTreeSet<_>>();
        assert_eq!(
            listed, files,
            "manifest must list every golden fixture exactly once"
        );
    }
}