1use crate::error::{Error, Result};
6use serde_json::Value;
7use std::path::Path;
8use tracing::debug;
9
10#[derive(Debug)]
12pub struct Schema {
13 name: String,
15
16 schema: Value,
18
19 compiled: jsonschema::JSONSchema,
21}
22
23impl Clone for Schema {
24 fn clone(&self) -> Self {
25 Self::new(self.name.clone(), self.schema.clone())
27 .expect("Schema was valid before, should be valid now")
28 }
29}
30
31impl Schema {
32 pub fn new(name: impl Into<String>, schema: Value) -> Result<Self> {
38 let name = name.into();
39 debug!(%name, "Creating schema");
40
41 let compiled =
42 jsonschema::JSONSchema::compile(&schema).map_err(|e| Error::InvalidSchema {
43 message: e.to_string(),
44 })?;
45
46 Ok(Self {
47 name,
48 schema,
49 compiled,
50 })
51 }
52
53 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
59 let path = path.as_ref();
60 let content = std::fs::read_to_string(path)?;
61 let schema: Value = serde_json::from_str(&content)?;
62
63 let name = path
64 .file_stem()
65 .and_then(|s| s.to_str())
66 .unwrap_or("unknown")
67 .to_string();
68
69 Self::new(name, schema)
70 }
71
72 #[must_use]
74 pub fn name(&self) -> &str {
75 &self.name
76 }
77
78 pub fn validate(&self, value: &Value) -> ValidationResult {
84 let mut errors = Vec::new();
85
86 if let Err(validation_errors) = self.compiled.validate(value) {
87 for error in validation_errors {
88 errors.push(ValidationError {
89 message: error.to_string(),
90 path: error.instance_path.to_string(),
91 keyword: format!("{:?}", error.kind),
92 });
93 }
94 }
95
96 ValidationResult { errors }
97 }
98
99 #[must_use]
101 pub fn is_valid(&self, value: &Value) -> bool {
102 self.compiled.is_valid(value)
103 }
104
105 #[must_use]
107 pub fn definition(&self) -> &Value {
108 &self.schema
109 }
110}
111
112#[derive(Debug, Default)]
114pub struct ValidationResult {
115 pub errors: Vec<ValidationError>,
117}
118
119impl ValidationResult {
120 #[must_use]
122 pub fn is_valid(&self) -> bool {
123 self.errors.is_empty()
124 }
125
126 #[must_use]
128 pub fn error_count(&self) -> usize {
129 self.errors.len()
130 }
131
132 pub fn into_result(self) -> Result<()> {
138 if self.is_valid() {
139 Ok(())
140 } else {
141 let message = self
142 .errors
143 .iter()
144 .map(|e| e.to_string())
145 .collect::<Vec<_>>()
146 .join("; ");
147 Err(Error::ValidationFailed {
148 message,
149 path: self.errors.first().map(|e| e.path.clone()),
150 })
151 }
152 }
153}
154
155#[derive(Debug, Clone)]
157pub struct ValidationError {
158 pub message: String,
160
161 pub path: String,
163
164 pub keyword: String,
166}
167
168impl std::fmt::Display for ValidationError {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 if self.path.is_empty() {
171 write!(f, "{}", self.message)
172 } else {
173 write!(f, "{}: {}", self.path, self.message)
174 }
175 }
176}
177
178#[must_use]
180pub fn article_schema() -> Value {
181 serde_json::json!({
182 "$schema": "https://json-schema.org/draft/2020-12/schema",
183 "type": "object",
184 "properties": {
185 "title": {
186 "type": "string",
187 "minLength": 1
188 },
189 "authors": {
190 "type": "array",
191 "items": {
192 "type": "object",
193 "properties": {
194 "name": { "type": "string" },
195 "email": { "type": "string", "format": "email" },
196 "affiliation": { "type": "string" },
197 "orcid": { "type": "string", "pattern": "^\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX]$" }
198 },
199 "required": ["name"]
200 }
201 },
202 "date": { "type": "string" },
203 "abstract": { "type": "string" },
204 "keywords": {
205 "type": "array",
206 "items": { "type": "string" }
207 },
208 "template": { "type": "string" },
209 "citation_style": { "type": "string" },
210 "bibliography": {
211 "type": "array",
212 "items": { "type": "string" }
213 }
214 },
215 "required": ["title"]
216 })
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn schema_validate_valid_document() {
225 let schema = Schema::new("test", article_schema()).unwrap();
226
227 let valid_doc = serde_json::json!({
228 "title": "My Document",
229 "authors": [{ "name": "John Doe" }]
230 });
231
232 let result = schema.validate(&valid_doc);
233 assert!(result.is_valid());
234 }
235
236 #[test]
237 fn schema_validate_missing_required() {
238 let schema = Schema::new("test", article_schema()).unwrap();
239
240 let invalid_doc = serde_json::json!({
241 "authors": [{ "name": "John Doe" }]
242 });
243
244 let result = schema.validate(&invalid_doc);
245 assert!(!result.is_valid());
246 assert_eq!(result.error_count(), 1);
247 }
248
249 #[test]
250 fn schema_validate_invalid_type() {
251 let schema = Schema::new("test", article_schema()).unwrap();
252
253 let invalid_doc = serde_json::json!({
254 "title": 123 });
256
257 let result = schema.validate(&invalid_doc);
258 assert!(!result.is_valid());
259 }
260
261 #[test]
262 fn schema_is_valid_shortcut() {
263 let schema = Schema::new("test", article_schema()).unwrap();
264
265 let valid = serde_json::json!({ "title": "Test" });
266 let invalid = serde_json::json!({ "title": "" });
267
268 assert!(schema.is_valid(&valid));
269 assert!(!schema.is_valid(&invalid));
270 }
271
272 #[test]
273 fn validation_result_into_result() {
274 let valid_result = ValidationResult::default();
275 assert!(valid_result.into_result().is_ok());
276
277 let invalid_result = ValidationResult {
278 errors: vec![ValidationError {
279 message: "test error".to_string(),
280 path: "/title".to_string(),
281 keyword: "required".to_string(),
282 }],
283 };
284 assert!(invalid_result.into_result().is_err());
285 }
286
287 #[test]
288 fn validation_error_display() {
289 let error = ValidationError {
290 message: "is required".to_string(),
291 path: "/title".to_string(),
292 keyword: "required".to_string(),
293 };
294 assert_eq!(error.to_string(), "/title: is required");
295
296 let error_no_path = ValidationError {
297 message: "invalid type".to_string(),
298 path: String::new(),
299 keyword: "type".to_string(),
300 };
301 assert_eq!(error_no_path.to_string(), "invalid type");
302 }
303}