1use 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#[derive(Debug, Clone, thiserror::Error)]
24pub enum MifSchemaError {
25 #[error("internal error: vendored MIF schema failed to compile: {0}")]
29 SchemaCompilation(String),
30 #[error("MIF document failed schema validation ({} error(s))", .0.len())]
32 Invalid(Vec<String>),
33}
34
35impl MifSchemaError {
36 #[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(®istry)
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
102pub fn validate_document(instance: &Value) -> Result<(), MifSchemaError> {
112 validate(document_validator()?, instance)
113}
114
115pub fn validate_citation(instance: &Value) -> Result<(), MifSchemaError> {
121 validate(citation_validator()?, instance)
122}
123
124pub 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}