Skip to main content

commonmeta_schema/
lib.rs

1#![forbid(unsafe_code)]
2
3use include_dir::{include_dir, Dir};
4
5/// The complete tree of Commonmeta conformance fixtures, embedded at compile
6/// time. Organized as `<format>/<name>.<ext>` (e.g. `commonmeta/journal_article.json`,
7/// `crossref/journal_article.json`, `orcid/0000-0002-0068-716X.xml`).
8///
9/// Consumers can look up a single file with [`fixture_str`] / [`fixture_bytes`],
10/// or iterate a subtree via `FIXTURES.get_dir("commonmeta").unwrap().files()`.
11pub static FIXTURES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/fixtures");
12
13/// Return an embedded fixture's raw bytes by relative path, e.g.
14/// `fixture_bytes("commonmeta/journal_article.json")`.
15pub fn fixture_bytes(path: &str) -> Option<&'static [u8]> {
16    FIXTURES.get_file(path).map(|f| f.contents())
17}
18
19/// Return an embedded fixture's UTF-8 contents by relative path, e.g.
20/// `fixture_str("crossref/journal_article.json")`. Returns `None` if the file
21/// is absent or not valid UTF-8.
22pub fn fixture_str(path: &str) -> Option<&'static str> {
23    FIXTURES.get_file(path).and_then(|f| f.contents_utf8())
24}
25
26/// Embedded Commonmeta schema v1.0. Records stamp `schema_version` as
27/// `https://commonmeta.org/commonmeta_v1.0.json`.
28pub const COMMONMETA_SCHEMA_V1_0: &str = include_str!("../schemas/commonmeta_v1.0.json");
29
30/// Return an embedded schema by Commonmeta version string.
31///
32/// Currently supported: "1.0".
33pub fn schema(version: &str) -> Option<&'static str> {
34    match version {
35        "1.0" => Some(COMMONMETA_SCHEMA_V1_0),
36        _ => None,
37    }
38}
39
40#[cfg(test)]
41mod tests {
42    use super::{schema, FIXTURES};
43    use jsonschema::validator_for;
44    use serde_json::{json, Value};
45
46    #[test]
47    fn known_version_is_available() {
48        assert!(schema("1.0").is_some());
49    }
50
51    #[test]
52    fn unknown_version_is_none() {
53        assert!(schema("nope").is_none());
54    }
55
56    #[test]
57    fn embedded_fixtures_are_available() {
58        use super::{fixture_bytes, fixture_str};
59        // A known commonmeta fixture is reachable by relative path.
60        assert!(fixture_str("commonmeta/journal_article.json").is_some());
61        // A non-JSON fixture (ORCID XML) is reachable as bytes.
62        assert!(fixture_bytes("orcid_xml/0000-0002-0068-716X.xml").is_some());
63        // Subtree iteration works and the commonmeta dir is non-empty.
64        let commonmeta = FIXTURES.get_dir("commonmeta").expect("commonmeta dir");
65        assert!(commonmeta.files().count() > 0);
66        // Absent paths return None rather than panicking.
67        assert!(fixture_str("nope/missing.json").is_none());
68    }
69
70    #[test]
71    fn all_embedded_commonmeta_fixtures_validate_against_exported_schema() {
72        let schema_text = schema("1.0").expect("schema 1.0 is embedded");
73        let schema_json: Value = serde_json::from_str(schema_text).expect("valid schema json");
74        let defs = schema_json
75            .get("$defs")
76            .cloned()
77            .expect("schema exposes $defs");
78        let entity_schema = json!({
79            "$schema": "https://json-schema.org/draft/2020-12/schema",
80            "$ref": "#/$defs/entity",
81            "$defs": defs
82        });
83        let validator = validator_for(&entity_schema).expect("entity schema compiles for validation");
84
85        let commonmeta_dir = FIXTURES.get_dir("commonmeta").expect("commonmeta dir");
86        let mut validated_count = 0usize;
87        let mut failures: Vec<String> = Vec::new();
88
89        for file in commonmeta_dir.files() {
90            let path = file.path().to_string_lossy();
91            if !path.ends_with(".json") {
92                continue;
93            }
94
95            validated_count += 1;
96
97            let text = match file.contents_utf8() {
98                Some(text) => text,
99                None => {
100                    failures.push(format!("{path}: fixture is not valid UTF-8"));
101                    continue;
102                }
103            };
104
105            let instance: Value = match serde_json::from_str(text) {
106                Ok(value) => value,
107                Err(err) => {
108                    failures.push(format!("{path}: invalid JSON: {err}"));
109                    continue;
110                }
111            };
112
113            let details: Vec<String> = validator
114                .iter_errors(&instance)
115                .take(5)
116                .map(|error| format!("{}: {}", error.instance_path, error))
117                .collect();
118            if !details.is_empty() {
119                failures.push(format!("{path}: {}", details.join(" | ")));
120            }
121        }
122
123        assert!(validated_count > 0, "no commonmeta fixtures found to validate");
124        assert!(
125            failures.is_empty(),
126            "{} fixture(s) failed schema validation:\n{}",
127            failures.len(),
128            failures.join("\n")
129        );
130    }
131}