commonmeta-schema 1.0.0-rc15

Commonmeta JSON Schemas and conformance fixtures
Documentation
#![forbid(unsafe_code)]

use include_dir::{include_dir, Dir};

/// The complete tree of Commonmeta conformance fixtures, embedded at compile
/// time. Organized as `<format>/<name>.<ext>` (e.g. `commonmeta/journal_article.json`,
/// `crossref/journal_article.json`, `orcid/0000-0002-0068-716X.xml`).
///
/// Consumers can look up a single file with [`fixture_str`] / [`fixture_bytes`],
/// or iterate a subtree via `FIXTURES.get_dir("commonmeta").unwrap().files()`.
pub static FIXTURES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/fixtures");

/// Return an embedded fixture's raw bytes by relative path, e.g.
/// `fixture_bytes("commonmeta/journal_article.json")`.
pub fn fixture_bytes(path: &str) -> Option<&'static [u8]> {
    FIXTURES.get_file(path).map(|f| f.contents())
}

/// Return an embedded fixture's UTF-8 contents by relative path, e.g.
/// `fixture_str("crossref/journal_article.json")`. Returns `None` if the file
/// is absent or not valid UTF-8.
pub fn fixture_str(path: &str) -> Option<&'static str> {
    FIXTURES.get_file(path).and_then(|f| f.contents_utf8())
}

/// Embedded Commonmeta schema v1.0. Records stamp `schema_version` as
/// `https://commonmeta.org/commonmeta_v1.0.json`.
pub const COMMONMETA_SCHEMA_V1_0: &str = include_str!("../schemas/commonmeta_v1.0.json");

/// Return an embedded schema by Commonmeta version string.
///
/// Currently supported: "1.0".
pub fn schema(version: &str) -> Option<&'static str> {
    match version {
        "1.0" => Some(COMMONMETA_SCHEMA_V1_0),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::{schema, FIXTURES};
    use jsonschema::validator_for;
    use serde_json::{json, Value};

    #[test]
    fn known_version_is_available() {
        assert!(schema("1.0").is_some());
    }

    #[test]
    fn unknown_version_is_none() {
        assert!(schema("nope").is_none());
    }

    #[test]
    fn embedded_fixtures_are_available() {
        use super::{fixture_bytes, fixture_str};
        // A known commonmeta fixture is reachable by relative path.
        assert!(fixture_str("commonmeta/journal_article.json").is_some());
        // A non-JSON fixture (ORCID XML) is reachable as bytes.
        assert!(fixture_bytes("orcid/0000-0002-0068-716X.xml").is_some());
        // Subtree iteration works and the commonmeta dir is non-empty.
        let commonmeta = FIXTURES.get_dir("commonmeta").expect("commonmeta dir");
        assert!(commonmeta.files().count() > 0);
        // Absent paths return None rather than panicking.
        assert!(fixture_str("nope/missing.json").is_none());
    }

    #[test]
    fn all_embedded_commonmeta_fixtures_validate_against_exported_schema() {
        let schema_text = schema("1.0").expect("schema 1.0 is embedded");
        let schema_json: Value = serde_json::from_str(schema_text).expect("valid schema json");
        let defs = schema_json
            .get("$defs")
            .cloned()
            .expect("schema exposes $defs");
        let entity_schema = json!({
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "$ref": "#/$defs/entity",
            "$defs": defs
        });
        let validator = validator_for(&entity_schema).expect("entity schema compiles for validation");

        let commonmeta_dir = FIXTURES.get_dir("commonmeta").expect("commonmeta dir");
        let mut validated_count = 0usize;
        let mut failures: Vec<String> = Vec::new();

        for file in commonmeta_dir.files() {
            let path = file.path().to_string_lossy();
            if !path.ends_with(".json") {
                continue;
            }

            validated_count += 1;

            let text = match file.contents_utf8() {
                Some(text) => text,
                None => {
                    failures.push(format!("{path}: fixture is not valid UTF-8"));
                    continue;
                }
            };

            let instance: Value = match serde_json::from_str(text) {
                Ok(value) => value,
                Err(err) => {
                    failures.push(format!("{path}: invalid JSON: {err}"));
                    continue;
                }
            };

            let details: Vec<String> = validator
                .iter_errors(&instance)
                .take(5)
                .map(|error| format!("{}: {}", error.instance_path, error))
                .collect();
            if !details.is_empty() {
                failures.push(format!("{path}: {}", details.join(" | ")));
            }
        }

        assert!(validated_count > 0, "no commonmeta fixtures found to validate");
        assert!(
            failures.is_empty(),
            "{} fixture(s) failed schema validation:\n{}",
            failures.len(),
            failures.join("\n")
        );
    }
}