1use std::path::Path;
8use std::sync::Arc;
9
10use crate::error::{Error, Result};
11use crate::value::Value;
12
13#[derive(Debug, Clone)]
15pub struct Schema {
16 schema: serde_json::Value,
18 compiled: Arc<jsonschema::Validator>,
20}
21
22impl Schema {
23 pub fn from_json(json: &str) -> Result<Self> {
25 let schema: serde_json::Value = serde_json::from_str(json)
26 .map_err(|e| Error::parse(format!("Invalid JSON schema: {}", e)))?;
27 Self::from_value(schema)
28 }
29
30 pub fn from_yaml(yaml: &str) -> Result<Self> {
32 let schema: serde_json::Value = serde_yaml::from_str(yaml)
33 .map_err(|e| Error::parse(format!("Invalid YAML schema: {}", e)))?;
34 Self::from_value(schema)
35 }
36
37 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
39 let path = path.as_ref();
40 let content = std::fs::read_to_string(path)
41 .map_err(|_| Error::file_not_found(path.display().to_string(), None))?;
42
43 match path.extension().and_then(|e| e.to_str()) {
44 Some("json") => Self::from_json(&content),
45 Some("yaml") | Some("yml") => Self::from_yaml(&content),
46 _ => Self::from_yaml(&content), }
48 }
49
50 fn from_value(schema: serde_json::Value) -> Result<Self> {
52 let compiled = jsonschema::validator_for(&schema)
53 .map_err(|e| Error::parse(format!("Invalid JSON Schema: {}", e)))?;
54 Ok(Self {
55 schema,
56 compiled: Arc::new(compiled),
57 })
58 }
59
60 pub fn validate(&self, value: &Value) -> Result<()> {
64 let json_value = value_to_json(value);
66
67 let mut errors = self.compiled.iter_errors(&json_value);
69 if let Some(error) = errors.next() {
70 let path = error.instance_path.to_string();
71 let message = error.to_string();
72 return Err(Error::validation(
73 if path.is_empty() { "<root>" } else { &path },
74 &message,
75 ));
76 }
77 Ok(())
78 }
79
80 pub fn validate_collect(&self, value: &Value) -> Vec<ValidationError> {
82 let json_value = value_to_json(value);
83
84 self.compiled
85 .iter_errors(&json_value)
86 .map(|e| ValidationError {
87 path: e.instance_path.to_string(),
88 message: e.to_string(),
89 })
90 .collect()
91 }
92
93 pub fn as_value(&self) -> &serde_json::Value {
95 &self.schema
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct ValidationError {
102 pub path: String,
104 pub message: String,
106}
107
108impl std::fmt::Display for ValidationError {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 if self.path.is_empty() {
111 write!(f, "{}", self.message)
112 } else {
113 write!(f, "{}: {}", self.path, self.message)
114 }
115 }
116}
117
118fn value_to_json(value: &Value) -> serde_json::Value {
120 match value {
121 Value::Null => serde_json::Value::Null,
122 Value::Bool(b) => serde_json::Value::Bool(*b),
123 Value::Integer(i) => serde_json::Value::Number((*i).into()),
124 Value::Float(f) => serde_json::Number::from_f64(*f)
125 .map(serde_json::Value::Number)
126 .unwrap_or(serde_json::Value::Null),
127 Value::String(s) => serde_json::Value::String(s.clone()),
128 Value::Sequence(seq) => serde_json::Value::Array(seq.iter().map(value_to_json).collect()),
129 Value::Mapping(map) => {
130 let obj: serde_json::Map<String, serde_json::Value> = map
131 .iter()
132 .map(|(k, v)| (k.clone(), value_to_json(v)))
133 .collect();
134 serde_json::Value::Object(obj)
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_schema_from_yaml() {
145 let schema_yaml = r#"
146type: object
147required:
148 - name
149properties:
150 name:
151 type: string
152 port:
153 type: integer
154 minimum: 1
155 maximum: 65535
156"#;
157 let schema = Schema::from_yaml(schema_yaml).unwrap();
158 assert!(schema.as_value().is_object());
159 }
160
161 #[test]
162 fn test_validate_valid_config() {
163 let schema = Schema::from_yaml(
164 r#"
165type: object
166properties:
167 name:
168 type: string
169 port:
170 type: integer
171"#,
172 )
173 .unwrap();
174
175 let mut map = indexmap::IndexMap::new();
176 map.insert("name".into(), Value::String("myapp".into()));
177 map.insert("port".into(), Value::Integer(8080));
178 let config = Value::Mapping(map);
179
180 assert!(schema.validate(&config).is_ok());
181 }
182
183 #[test]
184 fn test_validate_missing_required() {
185 let schema = Schema::from_yaml(
186 r#"
187type: object
188required:
189 - name
190properties:
191 name:
192 type: string
193"#,
194 )
195 .unwrap();
196
197 let config = Value::Mapping(indexmap::IndexMap::new());
198 let result = schema.validate(&config);
199 assert!(result.is_err());
200 let err = result.unwrap_err();
201 assert!(err.to_string().contains("name"));
202 }
203
204 #[test]
205 fn test_validate_wrong_type() {
206 let schema = Schema::from_yaml(
207 r#"
208type: object
209properties:
210 port:
211 type: integer
212"#,
213 )
214 .unwrap();
215
216 let mut map = indexmap::IndexMap::new();
217 map.insert("port".into(), Value::String("not-a-number".into()));
218 let config = Value::Mapping(map);
219
220 let result = schema.validate(&config);
221 assert!(result.is_err());
222 }
223
224 #[test]
225 fn test_validate_constraint_violation() {
226 let schema = Schema::from_yaml(
227 r#"
228type: object
229properties:
230 port:
231 type: integer
232 minimum: 1
233 maximum: 65535
234"#,
235 )
236 .unwrap();
237
238 let mut map = indexmap::IndexMap::new();
239 map.insert("port".into(), Value::Integer(70000));
240 let config = Value::Mapping(map);
241
242 let result = schema.validate(&config);
243 assert!(result.is_err());
244 }
245
246 #[test]
247 fn test_validate_enum() {
248 let schema = Schema::from_yaml(
249 r#"
250type: object
251properties:
252 log_level:
253 type: string
254 enum: [debug, info, warn, error]
255"#,
256 )
257 .unwrap();
258
259 let mut map = indexmap::IndexMap::new();
261 map.insert("log_level".into(), Value::String("info".into()));
262 let config = Value::Mapping(map);
263 assert!(schema.validate(&config).is_ok());
264
265 let mut map = indexmap::IndexMap::new();
267 map.insert("log_level".into(), Value::String("verbose".into()));
268 let config = Value::Mapping(map);
269 assert!(schema.validate(&config).is_err());
270 }
271
272 #[test]
273 fn test_validate_nested() {
274 let schema = Schema::from_yaml(
275 r#"
276type: object
277properties:
278 database:
279 type: object
280 required: [host]
281 properties:
282 host:
283 type: string
284 port:
285 type: integer
286 default: 5432
287"#,
288 )
289 .unwrap();
290
291 let mut db = indexmap::IndexMap::new();
293 db.insert("host".into(), Value::String("localhost".into()));
294 db.insert("port".into(), Value::Integer(5432));
295 let mut map = indexmap::IndexMap::new();
296 map.insert("database".into(), Value::Mapping(db));
297 let config = Value::Mapping(map);
298 assert!(schema.validate(&config).is_ok());
299
300 let db = indexmap::IndexMap::new();
302 let mut map = indexmap::IndexMap::new();
303 map.insert("database".into(), Value::Mapping(db));
304 let config = Value::Mapping(map);
305 assert!(schema.validate(&config).is_err());
306 }
307
308 #[test]
309 fn test_validate_collect_multiple_errors() {
310 let schema = Schema::from_yaml(
311 r#"
312type: object
313required:
314 - name
315 - port
316properties:
317 name:
318 type: string
319 port:
320 type: integer
321"#,
322 )
323 .unwrap();
324
325 let config = Value::Mapping(indexmap::IndexMap::new());
326 let errors = schema.validate_collect(&config);
327 assert!(!errors.is_empty());
329 }
330
331 #[test]
332 fn test_validate_additional_properties_allowed() {
333 let schema = Schema::from_yaml(
335 r#"
336type: object
337properties:
338 name:
339 type: string
340"#,
341 )
342 .unwrap();
343
344 let mut map = indexmap::IndexMap::new();
345 map.insert("name".into(), Value::String("myapp".into()));
346 map.insert("extra".into(), Value::String("allowed".into()));
347 let config = Value::Mapping(map);
348 assert!(schema.validate(&config).is_ok());
349 }
350
351 #[test]
352 fn test_validate_additional_properties_denied() {
353 let schema = Schema::from_yaml(
354 r#"
355type: object
356properties:
357 name:
358 type: string
359additionalProperties: false
360"#,
361 )
362 .unwrap();
363
364 let mut map = indexmap::IndexMap::new();
365 map.insert("name".into(), Value::String("myapp".into()));
366 map.insert("extra".into(), Value::String("not allowed".into()));
367 let config = Value::Mapping(map);
368 assert!(schema.validate(&config).is_err());
369 }
370
371 #[test]
372 fn test_validate_array() {
373 let schema = Schema::from_yaml(
374 r#"
375type: object
376properties:
377 servers:
378 type: array
379 items:
380 type: string
381"#,
382 )
383 .unwrap();
384
385 let mut map = indexmap::IndexMap::new();
386 map.insert(
387 "servers".into(),
388 Value::Sequence(vec![
389 Value::String("server1".into()),
390 Value::String("server2".into()),
391 ]),
392 );
393 let config = Value::Mapping(map);
394 assert!(schema.validate(&config).is_ok());
395
396 let mut map = indexmap::IndexMap::new();
398 map.insert(
399 "servers".into(),
400 Value::Sequence(vec![Value::String("server1".into()), Value::Integer(123)]),
401 );
402 let config = Value::Mapping(map);
403 assert!(schema.validate(&config).is_err());
404 }
405
406 #[test]
407 fn test_validate_pattern() {
408 let schema = Schema::from_yaml(
409 r#"
410type: object
411properties:
412 version:
413 type: string
414 pattern: "^\\d+\\.\\d+\\.\\d+$"
415"#,
416 )
417 .unwrap();
418
419 let mut map = indexmap::IndexMap::new();
421 map.insert("version".into(), Value::String("1.2.3".into()));
422 let config = Value::Mapping(map);
423 assert!(schema.validate(&config).is_ok());
424
425 let mut map = indexmap::IndexMap::new();
427 map.insert("version".into(), Value::String("v1.2".into()));
428 let config = Value::Mapping(map);
429 assert!(schema.validate(&config).is_err());
430 }
431}