Skip to main content

mif_schema/
lib.rs

1//! JSON Schema validation for the MIF (Modeled Information Format) ecosystem.
2//!
3//! Validates MIF documents and citation objects against the canonical MIF
4//! JSON Schema (draft 2020-12; see <https://mif-spec.dev/schema/>). Schemas
5//! are vendored at compile time (`src/schemas/`, synced from the canonical
6//! `MIF` repo) and resolved entirely offline — no network access happens at
7//! validation time.
8
9use std::sync::OnceLock;
10
11use jsonschema::{Registry, Validator};
12use serde_json::Value;
13
14const MIF_SCHEMA: &str = include_str!("schemas/mif.schema.json");
15const CITATION_SCHEMA: &str = include_str!("schemas/citation.schema.json");
16const ONTOLOGY_SCHEMA: &str = include_str!("schemas/ontology.schema.json");
17const ENTITY_REFERENCE_SCHEMA: &str =
18    include_str!("schemas/definitions/entity-reference.schema.json");
19const ENTITY_REFERENCE_SCHEMA_ID: &str =
20    "https://mif-spec.dev/schema/definitions/entity-reference.schema.json";
21
22/// Error validating a MIF document or citation against the canonical schema.
23#[derive(Debug, Clone, thiserror::Error)]
24pub enum MifSchemaError {
25    /// The vendored schema itself failed to compile. Indicates a bug in
26    /// this crate's vendored schema files, not a problem with the instance
27    /// being validated.
28    #[error("internal error: vendored MIF schema failed to compile: {0}")]
29    SchemaCompilation(String),
30    /// The instance failed schema validation.
31    #[error("MIF document failed schema validation ({} error(s))", .0.len())]
32    Invalid(Vec<String>),
33}
34
35impl MifSchemaError {
36    /// The individual validation error messages, if this is an
37    /// [`MifSchemaError::Invalid`]. Empty for [`MifSchemaError::SchemaCompilation`].
38    #[must_use]
39    pub fn messages(&self) -> &[String] {
40        match self {
41            Self::Invalid(errors) => errors,
42            Self::SchemaCompilation(_) => &[],
43        }
44    }
45}
46
47fn build_registry() -> Result<Registry<'static>, String> {
48    let entity_reference: Value =
49        serde_json::from_str(ENTITY_REFERENCE_SCHEMA).map_err(|e| e.to_string())?;
50    Registry::new()
51        .add(ENTITY_REFERENCE_SCHEMA_ID, entity_reference)
52        .map_err(|e| e.to_string())?
53        .prepare()
54        .map_err(|e| e.to_string())
55}
56
57fn build_validator(schema_json: &str) -> Result<Validator, String> {
58    let schema: Value = serde_json::from_str(schema_json).map_err(|e| e.to_string())?;
59    let registry = build_registry()?;
60    jsonschema::options()
61        .with_registry(&registry)
62        .build(&schema)
63        .map_err(|e| e.to_string())
64}
65
66fn document_validator() -> Result<&'static Validator, MifSchemaError> {
67    static VALIDATOR: OnceLock<Result<Validator, String>> = OnceLock::new();
68    VALIDATOR
69        .get_or_init(|| build_validator(MIF_SCHEMA))
70        .as_ref()
71        .map_err(|e| MifSchemaError::SchemaCompilation(e.clone()))
72}
73
74fn citation_validator() -> Result<&'static Validator, MifSchemaError> {
75    static VALIDATOR: OnceLock<Result<Validator, String>> = OnceLock::new();
76    VALIDATOR
77        .get_or_init(|| build_validator(CITATION_SCHEMA))
78        .as_ref()
79        .map_err(|e| MifSchemaError::SchemaCompilation(e.clone()))
80}
81
82fn ontology_validator() -> Result<&'static Validator, MifSchemaError> {
83    static VALIDATOR: OnceLock<Result<Validator, String>> = OnceLock::new();
84    VALIDATOR
85        .get_or_init(|| build_validator(ONTOLOGY_SCHEMA))
86        .as_ref()
87        .map_err(|e| MifSchemaError::SchemaCompilation(e.clone()))
88}
89
90fn validate(validator: &Validator, instance: &Value) -> Result<(), MifSchemaError> {
91    let errors: Vec<String> = validator
92        .iter_errors(instance)
93        .map(|error| error.to_string())
94        .collect();
95    if errors.is_empty() {
96        Ok(())
97    } else {
98        Err(MifSchemaError::Invalid(errors))
99    }
100}
101
102/// Validates a MIF document (a JSON-LD-projected memory) against the
103/// canonical `mif.schema.json`.
104///
105/// # Errors
106///
107/// Returns [`MifSchemaError::Invalid`] with every validation error message
108/// if `instance` does not conform to the schema, or
109/// [`MifSchemaError::SchemaCompilation`] if the vendored schema itself
110/// fails to compile (indicates a bug in this crate).
111pub fn validate_document(instance: &Value) -> Result<(), MifSchemaError> {
112    validate(document_validator()?, instance)
113}
114
115/// Validates a standalone MIF citation object against `citation.schema.json`.
116///
117/// # Errors
118///
119/// See [`validate_document`].
120pub fn validate_citation(instance: &Value) -> Result<(), MifSchemaError> {
121    validate(citation_validator()?, instance)
122}
123
124/// Validates an ontology definition object against `ontology.schema.json`.
125///
126/// # Errors
127///
128/// See [`validate_document`].
129pub fn validate_ontology_definition(instance: &Value) -> Result<(), MifSchemaError> {
130    validate(ontology_validator()?, instance)
131}
132
133#[cfg(test)]
134mod tests {
135    use serde_json::json;
136
137    use super::{validate_citation, validate_document, validate_ontology_definition};
138
139    fn minimal_valid_document() -> serde_json::Value {
140        json!({
141            "@context": "https://mif-spec.dev/schema/context.jsonld",
142            "@type": "Concept",
143            "@id": "urn:mif:memory:test-001",
144            "conceptType": "semantic",
145            "content": "Test content.",
146            "created": "2026-07-02T00:00:00Z",
147        })
148    }
149
150    #[test]
151    fn valid_document_passes() {
152        assert!(validate_document(&minimal_valid_document()).is_ok());
153    }
154
155    #[test]
156    fn document_missing_required_field_fails() {
157        let mut instance = minimal_valid_document();
158        instance.as_object_mut().unwrap().remove("conceptType");
159        let result = validate_document(&instance);
160        assert!(result.is_err());
161    }
162
163    #[test]
164    fn document_with_bad_id_pattern_fails() {
165        let mut instance = minimal_valid_document();
166        instance["@id"] = json!("not-a-urn");
167        assert!(validate_document(&instance).is_err());
168    }
169
170    #[test]
171    fn document_with_entity_reference_resolves_ref_chain() {
172        let mut instance = minimal_valid_document();
173        instance["entities"] = json!([{
174            "@type": "EntityReference",
175            "entity": { "@id": "urn:mif:entity:person:jane-smith" },
176            "entityType": "Person",
177        }]);
178        assert!(validate_document(&instance).is_ok());
179    }
180
181    #[test]
182    fn document_with_invalid_entity_reference_fails() {
183        let mut instance = minimal_valid_document();
184        instance["entities"] = json!([{
185            "@type": "EntityReference",
186            "entity": { "@id": "not-a-urn" },
187        }]);
188        assert!(validate_document(&instance).is_err());
189    }
190
191    #[test]
192    fn valid_citation_passes() {
193        let citation = json!({
194            "@type": "Citation",
195            "citationType": "documentation",
196            "citationRole": "source",
197            "title": "MIF Specification",
198            "url": "https://mif-spec.dev",
199        });
200        assert!(validate_citation(&citation).is_ok());
201    }
202
203    #[test]
204    fn valid_ontology_definition_passes() {
205        let ontology = json!({
206            "ontology": {
207                "id": "mif-base",
208                "version": "1.0.0",
209            }
210        });
211        assert!(validate_ontology_definition(&ontology).is_ok());
212    }
213
214    #[test]
215    fn ontology_definition_with_bad_id_pattern_fails() {
216        let ontology = json!({
217            "ontology": {
218                "id": "Not_Valid",
219                "version": "1.0.0",
220            }
221        });
222        assert!(validate_ontology_definition(&ontology).is_err());
223    }
224
225    #[test]
226    fn citation_missing_required_field_fails() {
227        let citation = json!({
228            "@type": "Citation",
229            "citationType": "documentation",
230            "citationRole": "source",
231            "title": "MIF Specification",
232        });
233        assert!(validate_citation(&citation).is_err());
234    }
235}