1use serde_json::{json, Map, Value};
25
26use crate::error::{Result, SammError};
27use crate::metamodel::{Aspect, Characteristic, CharacteristicKind, ModelElement, Property};
28
29#[derive(Debug, Clone)]
35pub struct JsonSchemaOptions {
36 pub include_descriptions: bool,
38 pub include_examples: bool,
40 pub use_defs_keyword: bool,
42 pub language: String,
44}
45
46impl Default for JsonSchemaOptions {
47 fn default() -> Self {
48 Self {
49 include_descriptions: true,
50 include_examples: true,
51 use_defs_keyword: true,
52 language: "en".to_string(),
53 }
54 }
55}
56
57#[derive(Debug, Default, Clone)]
66pub struct JsonSchemaGenerator {
67 options: JsonSchemaOptions,
68}
69
70impl JsonSchemaGenerator {
71 pub fn new() -> Self {
73 Self {
74 options: JsonSchemaOptions::default(),
75 }
76 }
77
78 pub fn with_descriptions(mut self) -> Self {
80 self.options.include_descriptions = true;
81 self
82 }
83
84 pub fn with_examples(mut self) -> Self {
86 self.options.include_examples = true;
87 self
88 }
89
90 pub fn without_descriptions(mut self) -> Self {
92 self.options.include_descriptions = false;
93 self
94 }
95
96 pub fn without_examples(mut self) -> Self {
98 self.options.include_examples = false;
99 self
100 }
101
102 pub fn with_language(mut self, lang: impl Into<String>) -> Self {
104 self.options.language = lang.into();
105 self
106 }
107
108 pub fn generate(&self, aspect: &Aspect) -> Result<Value> {
117 let mut root = Map::new();
118
119 let schema_keyword = if self.options.use_defs_keyword {
121 "https://json-schema.org/draft/2020-12/schema"
122 } else {
123 "http://json-schema.org/draft-07/schema#"
124 };
125 root.insert(
126 "$schema".to_string(),
127 Value::String(schema_keyword.to_string()),
128 );
129 root.insert("$id".to_string(), Value::String(aspect.urn().to_string()));
130
131 let aspect_name = aspect.name();
133 let title = aspect
134 .metadata()
135 .get_preferred_name(&self.options.language)
136 .map(|s| s.to_string())
137 .unwrap_or_else(|| aspect_name.clone());
138 root.insert("title".to_string(), Value::String(title));
139
140 if self.options.include_descriptions {
142 if let Some(desc) = aspect.metadata().get_description(&self.options.language) {
143 root.insert("description".to_string(), Value::String(desc.to_string()));
144 }
145 }
146
147 root.insert("type".to_string(), Value::String("object".to_string()));
148
149 let (properties_map, required) = self.build_properties(aspect.properties())?;
151 root.insert("properties".to_string(), Value::Object(properties_map));
152 if !required.is_empty() {
153 root.insert(
154 "required".to_string(),
155 Value::Array(required.into_iter().map(Value::String).collect()),
156 );
157 }
158
159 root.insert("additionalProperties".to_string(), Value::Bool(false));
161
162 Ok(Value::Object(root))
163 }
164
165 fn build_properties(&self, props: &[Property]) -> Result<(Map<String, Value>, Vec<String>)> {
173 let mut properties_map = Map::new();
174 let mut required = Vec::new();
175
176 for prop in props {
177 let name = prop_json_name(prop);
178 let prop_schema = self.property_to_schema(prop)?;
179 properties_map.insert(name.clone(), prop_schema);
180
181 if !prop.optional {
182 required.push(name);
183 }
184 }
185
186 Ok((properties_map, required))
187 }
188
189 fn property_to_schema(&self, prop: &Property) -> Result<Value> {
191 let mut schema = Map::new();
192
193 if let Some(name) = prop.metadata().get_preferred_name(&self.options.language) {
195 schema.insert("title".to_string(), Value::String(name.to_string()));
196 }
197
198 if self.options.include_descriptions {
200 if let Some(desc) = prop.metadata().get_description(&self.options.language) {
201 schema.insert("description".to_string(), Value::String(desc.to_string()));
202 }
203 }
204
205 if let Some(char) = &prop.characteristic {
207 let type_schema = self.characteristic_to_schema(char)?;
208 if let Value::Object(type_map) = type_schema {
210 for (k, v) in type_map {
211 schema.insert(k, v);
212 }
213 }
214 } else {
215 schema.insert("type".to_string(), Value::String("string".to_string()));
217 }
218
219 if self.options.include_examples && !prop.example_values.is_empty() {
221 let examples: Vec<Value> = prop
222 .example_values
223 .iter()
224 .map(|v| Value::String(v.clone()))
225 .collect();
226 schema.insert("examples".to_string(), Value::Array(examples));
227 }
228
229 Ok(Value::Object(schema))
230 }
231
232 fn characteristic_to_schema(&self, char: &Characteristic) -> Result<Value> {
234 let schema = match char.kind() {
235 CharacteristicKind::Trait => {
236 let json_type = char
238 .data_type
239 .as_deref()
240 .map(|dt| self.data_type_to_json_type(dt))
241 .unwrap_or("string");
242 json!({ "type": json_type })
243 }
244
245 CharacteristicKind::Quantifiable { unit }
246 | CharacteristicKind::Measurement { unit } => {
247 let json_type = char
248 .data_type
249 .as_deref()
250 .map(|dt| self.data_type_to_json_type(dt))
251 .unwrap_or("number");
252 json!({
253 "type": json_type,
254 "description": format!("Value in {}", unit)
255 })
256 }
257
258 CharacteristicKind::Duration { unit } => {
259 json!({
260 "type": "number",
261 "description": format!("Duration value in {}", unit)
262 })
263 }
264
265 CharacteristicKind::Enumeration { values } => {
266 json!({ "enum": values })
267 }
268
269 CharacteristicKind::State {
270 values,
271 default_value,
272 } => {
273 let mut s = json!({ "enum": values });
274 if let (Some(map), Some(default)) = (s.as_object_mut(), default_value.as_deref()) {
275 map.insert("default".to_string(), Value::String(default.to_string()));
276 }
277 s
278 }
279
280 CharacteristicKind::Collection {
281 element_characteristic,
282 }
283 | CharacteristicKind::List {
284 element_characteristic,
285 }
286 | CharacteristicKind::TimeSeries {
287 element_characteristic,
288 } => {
289 let items = if let Some(inner) = element_characteristic {
290 self.characteristic_to_schema(inner)?
291 } else {
292 json!({})
293 };
294 json!({ "type": "array", "items": items })
295 }
296
297 CharacteristicKind::Set {
298 element_characteristic,
299 } => {
300 let items = if let Some(inner) = element_characteristic {
301 self.characteristic_to_schema(inner)?
302 } else {
303 json!({})
304 };
305 json!({ "type": "array", "items": items, "uniqueItems": true })
306 }
307
308 CharacteristicKind::SortedSet {
309 element_characteristic,
310 } => {
311 let items = if let Some(inner) = element_characteristic {
312 self.characteristic_to_schema(inner)?
313 } else {
314 json!({})
315 };
316 json!({ "type": "array", "items": items, "uniqueItems": true })
317 }
318
319 CharacteristicKind::Code => {
320 json!({ "type": "string" })
321 }
322
323 CharacteristicKind::Either { left, right } => {
324 let left_schema = self.characteristic_to_schema(left)?;
325 let right_schema = self.characteristic_to_schema(right)?;
326 json!({ "oneOf": [left_schema, right_schema] })
327 }
328
329 CharacteristicKind::SingleEntity { entity_type } => {
330 let ref_name = entity_type
332 .split('#')
333 .next_back()
334 .unwrap_or(entity_type.as_str());
335 let defs_key = if self.options.use_defs_keyword {
336 "$defs"
337 } else {
338 "definitions"
339 };
340 json!({ "$ref": format!("#{}/{}", defs_key, ref_name) })
341 }
342
343 CharacteristicKind::StructuredValue {
344 deconstruction_rule: _,
345 elements: _,
346 } => {
347 json!({ "type": "string", "format": "structured-value" })
349 }
350 };
351
352 let schema = self.apply_constraints(char, schema)?;
354
355 Ok(schema)
356 }
357
358 fn apply_constraints(&self, char: &Characteristic, mut schema: Value) -> Result<Value> {
360 use crate::metamodel::Constraint;
361
362 for constraint in &char.constraints {
363 if let Some(obj) = schema.as_object_mut() {
364 match constraint {
365 Constraint::RangeConstraint {
366 min_value,
367 max_value,
368 ..
369 } => {
370 if let Some(min) = min_value {
371 if let Ok(n) = min.parse::<f64>() {
372 obj.insert("minimum".to_string(), json!(n));
373 }
374 }
375 if let Some(max) = max_value {
376 if let Ok(n) = max.parse::<f64>() {
377 obj.insert("maximum".to_string(), json!(n));
378 }
379 }
380 }
381 Constraint::LengthConstraint {
382 min_value,
383 max_value,
384 } => {
385 if let Some(min) = min_value {
386 obj.insert("minLength".to_string(), json!(min));
387 }
388 if let Some(max) = max_value {
389 obj.insert("maxLength".to_string(), json!(max));
390 }
391 }
392 Constraint::RegularExpressionConstraint { pattern } => {
393 obj.insert("pattern".to_string(), Value::String(pattern.clone()));
394 }
395 Constraint::LanguageConstraint { .. } | Constraint::LocaleConstraint { .. } => {
396 }
398 Constraint::EncodingConstraint { encoding } => {
399 obj.insert(
400 "contentEncoding".to_string(),
401 Value::String(encoding.clone()),
402 );
403 }
404 Constraint::FixedPointConstraint { integer, scale } => {
405 let _ = (integer, scale); }
408 }
409 }
410 }
411 Ok(schema)
412 }
413
414 pub fn data_type_to_json_type(&self, data_type: &str) -> &'static str {
416 if data_type.ends_with("boolean") {
417 return "boolean";
418 }
419 if data_type.ends_with("int")
420 || data_type.ends_with("integer")
421 || data_type.ends_with("long")
422 || data_type.ends_with("short")
423 || data_type.ends_with("byte")
424 || data_type.ends_with("unsignedInt")
425 || data_type.ends_with("unsignedLong")
426 || data_type.ends_with("unsignedShort")
427 || data_type.ends_with("unsignedByte")
428 || data_type.ends_with("positiveInteger")
429 || data_type.ends_with("negativeInteger")
430 || data_type.ends_with("nonNegativeInteger")
431 || data_type.ends_with("nonPositiveInteger")
432 {
433 return "integer";
434 }
435 if data_type.ends_with("decimal")
436 || data_type.ends_with("float")
437 || data_type.ends_with("double")
438 {
439 return "number";
440 }
441 "string"
442 }
443}
444
445fn prop_json_name(prop: &Property) -> String {
452 prop.payload_name.clone().unwrap_or_else(|| prop.name())
453}
454
455#[derive(Debug, Clone, PartialEq)]
461pub struct ValidationError {
462 pub path: String,
464 pub message: String,
466 pub schema_path: String,
468}
469
470impl ValidationError {
471 pub fn new(
473 path: impl Into<String>,
474 message: impl Into<String>,
475 schema_path: impl Into<String>,
476 ) -> Self {
477 Self {
478 path: path.into(),
479 message: message.into(),
480 schema_path: schema_path.into(),
481 }
482 }
483}
484
485#[derive(Debug, Default, Clone)]
510pub struct JsonSchemaValidator;
511
512impl JsonSchemaValidator {
513 pub fn new() -> Self {
515 Self
516 }
517
518 pub fn validate(
522 &self,
523 schema: &serde_json::Value,
524 instance: &serde_json::Value,
525 ) -> Vec<ValidationError> {
526 self.validate_with_path(schema, instance, "", "")
527 }
528
529 fn validate_with_path(
531 &self,
532 schema: &serde_json::Value,
533 instance: &serde_json::Value,
534 path: &str,
535 schema_path: &str,
536 ) -> Vec<ValidationError> {
537 let mut errors = Vec::new();
538
539 if let Some(type_value) = schema.get("type") {
541 if let Some(type_str) = type_value.as_str() {
542 let type_ok = match type_str {
543 "string" => instance.is_string(),
544 "number" => instance.is_number(),
545 "integer" => instance.as_f64().map(|n| n.fract() == 0.0).unwrap_or(false),
546 "boolean" => instance.is_boolean(),
547 "array" => instance.is_array(),
548 "object" => instance.is_object(),
549 "null" => instance.is_null(),
550 _ => true, };
552 if !type_ok {
553 errors.push(ValidationError::new(
554 path,
555 format!(
556 "expected type '{}' but got '{}'",
557 type_str,
558 json_type_name(instance)
559 ),
560 format!("{}/type", schema_path),
561 ));
562 }
563 }
564 }
565
566 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
568 if let Some(obj) = instance.as_object() {
569 for req in required {
570 if let Some(field) = req.as_str() {
571 if !obj.contains_key(field) {
572 errors.push(ValidationError::new(
573 path,
574 format!("required property '{}' is missing", field),
575 format!("{}/required", schema_path),
576 ));
577 }
578 }
579 }
580 }
581 }
582
583 if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()) {
585 if !enum_values.iter().any(|e| e == instance) {
586 errors.push(ValidationError::new(
587 path,
588 "value is not one of the allowed enum values".to_string(),
589 format!("{}/enum", schema_path),
590 ));
591 }
592 }
593
594 if let Some(instance_num) = instance.as_f64() {
596 if let Some(min) = schema.get("minimum").and_then(|v| v.as_f64()) {
597 if instance_num < min {
598 errors.push(ValidationError::new(
599 path,
600 format!("value {} is less than minimum {}", instance_num, min),
601 format!("{}/minimum", schema_path),
602 ));
603 }
604 }
605 if let Some(max) = schema.get("maximum").and_then(|v| v.as_f64()) {
606 if instance_num > max {
607 errors.push(ValidationError::new(
608 path,
609 format!("value {} is greater than maximum {}", instance_num, max),
610 format!("{}/maximum", schema_path),
611 ));
612 }
613 }
614 }
615
616 if let Some(s) = instance.as_str() {
618 let char_count = s.chars().count();
619 if let Some(min_len) = schema.get("minLength").and_then(|v| v.as_u64()) {
620 if (char_count as u64) < min_len {
621 errors.push(ValidationError::new(
622 path,
623 format!(
624 "string length {} is less than minLength {}",
625 char_count, min_len
626 ),
627 format!("{}/minLength", schema_path),
628 ));
629 }
630 }
631 if let Some(max_len) = schema.get("maxLength").and_then(|v| v.as_u64()) {
632 if (char_count as u64) > max_len {
633 errors.push(ValidationError::new(
634 path,
635 format!("string length {} exceeds maxLength {}", char_count, max_len),
636 format!("{}/maxLength", schema_path),
637 ));
638 }
639 }
640 }
641
642 if let Some(add_props) = schema.get("additionalProperties") {
644 if add_props == &serde_json::Value::Bool(false) {
645 if let Some(obj) = instance.as_object() {
646 let allowed: std::collections::HashSet<&str> = schema
647 .get("properties")
648 .and_then(|p| p.as_object())
649 .map(|p| p.keys().map(|k| k.as_str()).collect())
650 .unwrap_or_default();
651
652 for key in obj.keys() {
653 if !allowed.contains(key.as_str()) {
654 let err_path = if path.is_empty() {
655 key.clone()
656 } else {
657 format!("{}/{}", path, key)
658 };
659 errors.push(ValidationError::new(
660 err_path,
661 format!("additional property '{}' is not allowed", key),
662 format!("{}/additionalProperties", schema_path),
663 ));
664 }
665 }
666 }
667 }
668 }
669
670 if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
672 if let Some(instance_obj) = instance.as_object() {
673 for (prop_key, prop_schema) in properties {
674 if let Some(prop_value) = instance_obj.get(prop_key) {
675 let child_path = if path.is_empty() {
676 format!("/{}", prop_key)
677 } else {
678 format!("{}/{}", path, prop_key)
679 };
680 let child_schema_path = format!("{}/properties/{}", schema_path, prop_key);
681 let child_errors = self.validate_with_path(
682 prop_schema,
683 prop_value,
684 &child_path,
685 &child_schema_path,
686 );
687 errors.extend(child_errors);
688 }
689 }
690 }
691 }
692
693 errors
694 }
695}
696
697fn json_type_name(v: &serde_json::Value) -> &'static str {
699 match v {
700 serde_json::Value::Null => "null",
701 serde_json::Value::Bool(_) => "boolean",
702 serde_json::Value::Number(_) => "number",
703 serde_json::Value::String(_) => "string",
704 serde_json::Value::Array(_) => "array",
705 serde_json::Value::Object(_) => "object",
706 }
707}
708
709#[cfg(test)]
714mod tests {
715 use super::*;
716 use crate::metamodel::{Aspect, Characteristic, CharacteristicKind, Property};
717
718 fn speed_aspect() -> Aspect {
719 let mut aspect = Aspect::new("urn:samm:org.example:1.0.0#Movement".to_string());
720 aspect
721 .metadata
722 .add_preferred_name("en".to_string(), "Movement".to_string());
723 aspect
724 .metadata
725 .add_description("en".to_string(), "Describes movement data".to_string());
726
727 let char = Characteristic::new(
728 "urn:samm:org.example:1.0.0#SpeedChar".to_string(),
729 CharacteristicKind::Measurement {
730 unit: "unit:kilometrePerHour".to_string(),
731 },
732 )
733 .with_data_type("http://www.w3.org/2001/XMLSchema#float".to_string());
734
735 let prop =
736 Property::new("urn:samm:org.example:1.0.0#speed".to_string()).with_characteristic(char);
737
738 aspect.add_property(prop);
739 aspect
740 }
741
742 #[test]
743 fn test_generate_basic_schema() {
744 let aspect = speed_aspect();
745 let gen = JsonSchemaGenerator::new();
746 let schema = gen.generate(&aspect).expect("generation should succeed");
747
748 assert_eq!(
749 schema["$schema"],
750 "https://json-schema.org/draft/2020-12/schema"
751 );
752 assert_eq!(schema["type"], "object");
753 assert!(schema["properties"]["speed"].is_object());
754 }
755
756 #[test]
757 fn test_schema_has_description() {
758 let aspect = speed_aspect();
759 let gen = JsonSchemaGenerator::new().with_descriptions();
760 let schema = gen.generate(&aspect).expect("generation should succeed");
761 assert_eq!(schema["description"], "Describes movement data");
762 }
763
764 #[test]
765 fn test_schema_no_description() {
766 let aspect = speed_aspect();
767 let gen = JsonSchemaGenerator::new().without_descriptions();
768 let schema = gen.generate(&aspect).expect("generation should succeed");
769 assert!(schema.get("description").is_none());
770 }
771
772 #[test]
773 fn test_required_non_optional_properties() {
774 let aspect = speed_aspect();
775 let gen = JsonSchemaGenerator::new();
776 let schema = gen.generate(&aspect).expect("generation should succeed");
777 let required = schema["required"]
778 .as_array()
779 .expect("required should be array");
780 assert!(required.iter().any(|v| v == "speed"));
781 }
782
783 #[test]
784 fn test_optional_not_in_required() {
785 let mut aspect = Aspect::new("urn:samm:org.example:1.0.0#TestAspect".to_string());
786 let char = Characteristic::new(
787 "urn:samm:org.example:1.0.0#Char".to_string(),
788 CharacteristicKind::Trait,
789 )
790 .with_data_type("http://www.w3.org/2001/XMLSchema#string".to_string());
791 let prop = Property::new("urn:samm:org.example:1.0.0#optProp".to_string())
792 .with_characteristic(char)
793 .as_optional();
794 aspect.add_property(prop);
795
796 let gen = JsonSchemaGenerator::new();
797 let schema = gen.generate(&aspect).expect("generation should succeed");
798 if let Some(arr) = schema.get("required").and_then(|v| v.as_array()) {
800 assert!(!arr.iter().any(|v| v == "optProp"));
801 }
802 }
803
804 #[test]
805 fn test_enumeration_generates_enum_keyword() {
806 let mut aspect = Aspect::new("urn:samm:org.example:1.0.0#TestAspect".to_string());
807 let char = Characteristic::new(
808 "urn:samm:org.example:1.0.0#StatusEnum".to_string(),
809 CharacteristicKind::Enumeration {
810 values: vec!["Active".to_string(), "Inactive".to_string()],
811 },
812 );
813 let prop = Property::new("urn:samm:org.example:1.0.0#status".to_string())
814 .with_characteristic(char);
815 aspect.add_property(prop);
816
817 let gen = JsonSchemaGenerator::new();
818 let schema = gen.generate(&aspect).expect("generation should succeed");
819 let status_prop = &schema["properties"]["status"];
820 assert!(status_prop["enum"].is_array());
821 let vals = status_prop["enum"].as_array().expect("enum is array");
822 assert_eq!(vals.len(), 2);
823 }
824
825 #[test]
826 fn test_collection_generates_array_type() {
827 let mut aspect = Aspect::new("urn:samm:org.example:1.0.0#TestAspect".to_string());
828 let inner = Characteristic::new(
829 "urn:samm:org.example:1.0.0#Inner".to_string(),
830 CharacteristicKind::Trait,
831 )
832 .with_data_type("http://www.w3.org/2001/XMLSchema#string".to_string());
833 let char = Characteristic::new(
834 "urn:samm:org.example:1.0.0#Names".to_string(),
835 CharacteristicKind::List {
836 element_characteristic: Some(Box::new(inner)),
837 },
838 );
839 let prop =
840 Property::new("urn:samm:org.example:1.0.0#names".to_string()).with_characteristic(char);
841 aspect.add_property(prop);
842
843 let gen = JsonSchemaGenerator::new();
844 let schema = gen.generate(&aspect).expect("generation should succeed");
845 assert_eq!(schema["properties"]["names"]["type"], "array");
846 }
847
848 #[test]
849 fn test_data_type_to_json_type_mapping() {
850 let gen = JsonSchemaGenerator::new();
851 assert_eq!(
852 gen.data_type_to_json_type("http://www.w3.org/2001/XMLSchema#boolean"),
853 "boolean"
854 );
855 assert_eq!(
856 gen.data_type_to_json_type("http://www.w3.org/2001/XMLSchema#int"),
857 "integer"
858 );
859 assert_eq!(
860 gen.data_type_to_json_type("http://www.w3.org/2001/XMLSchema#float"),
861 "number"
862 );
863 assert_eq!(
864 gen.data_type_to_json_type("http://www.w3.org/2001/XMLSchema#string"),
865 "string"
866 );
867 assert_eq!(gen.data_type_to_json_type("xsd:dateTime"), "string");
868 }
869
870 #[test]
871 fn test_either_generates_one_of() {
872 let left = Characteristic::new(
873 "urn:samm:org.example:1.0.0#Left".to_string(),
874 CharacteristicKind::Trait,
875 )
876 .with_data_type("http://www.w3.org/2001/XMLSchema#string".to_string());
877 let right = Characteristic::new(
878 "urn:samm:org.example:1.0.0#Right".to_string(),
879 CharacteristicKind::Trait,
880 )
881 .with_data_type("http://www.w3.org/2001/XMLSchema#int".to_string());
882
883 let char = Characteristic::new(
884 "urn:samm:org.example:1.0.0#EitherChar".to_string(),
885 CharacteristicKind::Either {
886 left: Box::new(left),
887 right: Box::new(right),
888 },
889 );
890
891 let gen = JsonSchemaGenerator::new();
892 let schema = gen
893 .characteristic_to_schema(&char)
894 .expect("generation should succeed");
895 assert!(schema["oneOf"].is_array());
896 assert_eq!(schema["oneOf"].as_array().map(|a| a.len()), Some(2));
897 }
898
899 #[test]
900 fn test_draft_07_schema_identifier() {
901 let gen = JsonSchemaGenerator {
902 options: JsonSchemaOptions {
903 use_defs_keyword: false,
904 ..Default::default()
905 },
906 };
907 let aspect = speed_aspect();
908 let schema = gen.generate(&aspect).expect("generation should succeed");
909 assert_eq!(schema["$schema"], "http://json-schema.org/draft-07/schema#");
910 }
911
912 #[test]
917 fn test_validator_valid_string_type() {
918 let v = JsonSchemaValidator::new();
919 let schema = serde_json::json!({ "type": "string" });
920 let instance = serde_json::json!("hello");
921 assert!(v.validate(&schema, &instance).is_empty());
922 }
923
924 #[test]
925 fn test_validator_invalid_string_type() {
926 let v = JsonSchemaValidator::new();
927 let schema = serde_json::json!({ "type": "string" });
928 let instance = serde_json::json!(42);
929 let errors = v.validate(&schema, &instance);
930 assert!(!errors.is_empty());
931 assert!(errors[0].message.contains("string"));
932 }
933
934 #[test]
935 fn test_validator_valid_number_type() {
936 let v = JsonSchemaValidator::new();
937 let schema = serde_json::json!({ "type": "number" });
938 assert!(v.validate(&schema, &serde_json::json!(3.5)).is_empty());
939 }
940
941 #[test]
942 fn test_validator_invalid_number_type() {
943 let v = JsonSchemaValidator::new();
944 let schema = serde_json::json!({ "type": "number" });
945 let errors = v.validate(&schema, &serde_json::json!("not a number"));
946 assert!(!errors.is_empty());
947 }
948
949 #[test]
950 fn test_validator_valid_integer_type() {
951 let v = JsonSchemaValidator::new();
952 let schema = serde_json::json!({ "type": "integer" });
953 assert!(v.validate(&schema, &serde_json::json!(7)).is_empty());
954 }
955
956 #[test]
957 fn test_validator_invalid_integer_type() {
958 let v = JsonSchemaValidator::new();
959 let schema = serde_json::json!({ "type": "integer" });
960 let errors = v.validate(&schema, &serde_json::json!(3.5));
962 assert!(!errors.is_empty());
963 }
964
965 #[test]
966 fn test_validator_valid_boolean_type() {
967 let v = JsonSchemaValidator::new();
968 let schema = serde_json::json!({ "type": "boolean" });
969 assert!(v.validate(&schema, &serde_json::json!(true)).is_empty());
970 }
971
972 #[test]
973 fn test_validator_invalid_boolean_type() {
974 let v = JsonSchemaValidator::new();
975 let schema = serde_json::json!({ "type": "boolean" });
976 let errors = v.validate(&schema, &serde_json::json!("true"));
977 assert!(!errors.is_empty());
978 }
979
980 #[test]
981 fn test_validator_valid_array_type() {
982 let v = JsonSchemaValidator::new();
983 let schema = serde_json::json!({ "type": "array" });
984 assert!(v
985 .validate(&schema, &serde_json::json!([1, 2, 3]))
986 .is_empty());
987 }
988
989 #[test]
990 fn test_validator_invalid_array_type() {
991 let v = JsonSchemaValidator::new();
992 let schema = serde_json::json!({ "type": "array" });
993 let errors = v.validate(&schema, &serde_json::json!({}));
994 assert!(!errors.is_empty());
995 }
996
997 #[test]
998 fn test_validator_valid_object_type() {
999 let v = JsonSchemaValidator::new();
1000 let schema = serde_json::json!({ "type": "object" });
1001 assert!(v.validate(&schema, &serde_json::json!({"a": 1})).is_empty());
1002 }
1003
1004 #[test]
1005 fn test_validator_invalid_object_type() {
1006 let v = JsonSchemaValidator::new();
1007 let schema = serde_json::json!({ "type": "object" });
1008 let errors = v.validate(&schema, &serde_json::json!([1, 2]));
1009 assert!(!errors.is_empty());
1010 }
1011
1012 #[test]
1013 fn test_validator_valid_null_type() {
1014 let v = JsonSchemaValidator::new();
1015 let schema = serde_json::json!({ "type": "null" });
1016 assert!(v.validate(&schema, &serde_json::json!(null)).is_empty());
1017 }
1018
1019 #[test]
1020 fn test_validator_required_fields_all_present() {
1021 let v = JsonSchemaValidator::new();
1022 let schema = serde_json::json!({
1023 "type": "object",
1024 "required": ["name", "age"]
1025 });
1026 let instance = serde_json::json!({ "name": "Alice", "age": 30 });
1027 assert!(v.validate(&schema, &instance).is_empty());
1028 }
1029
1030 #[test]
1031 fn test_validator_required_fields_missing() {
1032 let v = JsonSchemaValidator::new();
1033 let schema = serde_json::json!({
1034 "type": "object",
1035 "required": ["name"]
1036 });
1037 let instance = serde_json::json!({ "age": 30 });
1038 let errors = v.validate(&schema, &instance);
1039 assert!(!errors.is_empty());
1040 assert!(errors.iter().any(|e| e.message.contains("name")));
1041 }
1042
1043 #[test]
1044 fn test_validator_required_multiple_missing() {
1045 let v = JsonSchemaValidator::new();
1046 let schema = serde_json::json!({
1047 "type": "object",
1048 "required": ["a", "b", "c"]
1049 });
1050 let instance = serde_json::json!({});
1051 let errors = v.validate(&schema, &instance);
1052 assert!(errors.len() >= 3);
1054 }
1055
1056 #[test]
1057 fn test_validator_enum_valid() {
1058 let v = JsonSchemaValidator::new();
1059 let schema = serde_json::json!({ "enum": ["red", "green", "blue"] });
1060 assert!(v.validate(&schema, &serde_json::json!("green")).is_empty());
1061 }
1062
1063 #[test]
1064 fn test_validator_enum_invalid() {
1065 let v = JsonSchemaValidator::new();
1066 let schema = serde_json::json!({ "enum": ["red", "green", "blue"] });
1067 let errors = v.validate(&schema, &serde_json::json!("purple"));
1068 assert!(!errors.is_empty());
1069 assert!(errors[0].message.contains("enum"));
1070 }
1071
1072 #[test]
1073 fn test_validator_minimum_valid() {
1074 let v = JsonSchemaValidator::new();
1075 let schema = serde_json::json!({ "type": "number", "minimum": 0.0 });
1076 assert!(v.validate(&schema, &serde_json::json!(5)).is_empty());
1077 }
1078
1079 #[test]
1080 fn test_validator_minimum_violation() {
1081 let v = JsonSchemaValidator::new();
1082 let schema = serde_json::json!({ "type": "number", "minimum": 10 });
1083 let errors = v.validate(&schema, &serde_json::json!(5));
1084 assert!(!errors.is_empty());
1085 assert!(errors.iter().any(|e| e.schema_path.contains("minimum")));
1086 }
1087
1088 #[test]
1089 fn test_validator_maximum_valid() {
1090 let v = JsonSchemaValidator::new();
1091 let schema = serde_json::json!({ "type": "number", "maximum": 100 });
1092 assert!(v.validate(&schema, &serde_json::json!(50)).is_empty());
1093 }
1094
1095 #[test]
1096 fn test_validator_maximum_violation() {
1097 let v = JsonSchemaValidator::new();
1098 let schema = serde_json::json!({ "type": "number", "maximum": 10 });
1099 let errors = v.validate(&schema, &serde_json::json!(20));
1100 assert!(!errors.is_empty());
1101 assert!(errors.iter().any(|e| e.schema_path.contains("maximum")));
1102 }
1103
1104 #[test]
1105 fn test_validator_min_length_valid() {
1106 let v = JsonSchemaValidator::new();
1107 let schema = serde_json::json!({ "type": "string", "minLength": 3 });
1108 assert!(v.validate(&schema, &serde_json::json!("abcd")).is_empty());
1109 }
1110
1111 #[test]
1112 fn test_validator_min_length_violation() {
1113 let v = JsonSchemaValidator::new();
1114 let schema = serde_json::json!({ "type": "string", "minLength": 5 });
1115 let errors = v.validate(&schema, &serde_json::json!("hi"));
1116 assert!(!errors.is_empty());
1117 assert!(errors.iter().any(|e| e.schema_path.contains("minLength")));
1118 }
1119
1120 #[test]
1121 fn test_validator_max_length_violation() {
1122 let v = JsonSchemaValidator::new();
1123 let schema = serde_json::json!({ "type": "string", "maxLength": 3 });
1124 let errors = v.validate(&schema, &serde_json::json!("toolong"));
1125 assert!(!errors.is_empty());
1126 assert!(errors.iter().any(|e| e.schema_path.contains("maxLength")));
1127 }
1128
1129 #[test]
1130 fn test_validator_additional_properties_blocked() {
1131 let v = JsonSchemaValidator::new();
1132 let schema = serde_json::json!({
1133 "type": "object",
1134 "properties": { "name": { "type": "string" } },
1135 "additionalProperties": false
1136 });
1137 let instance = serde_json::json!({ "name": "Alice", "extra": "oops" });
1138 let errors = v.validate(&schema, &instance);
1139 assert!(!errors.is_empty());
1140 assert!(errors.iter().any(|e| e.message.contains("extra")));
1141 }
1142
1143 #[test]
1144 fn test_validator_additional_properties_allowed_when_not_false() {
1145 let v = JsonSchemaValidator::new();
1146 let schema = serde_json::json!({
1147 "type": "object",
1148 "properties": { "name": { "type": "string" } }
1149 });
1150 let instance = serde_json::json!({ "name": "Alice", "extra": "ok" });
1151 assert!(v.validate(&schema, &instance).is_empty());
1153 }
1154
1155 #[test]
1156 fn test_validator_nested_property_type_error() {
1157 let v = JsonSchemaValidator::new();
1158 let schema = serde_json::json!({
1159 "type": "object",
1160 "properties": {
1161 "user": {
1162 "type": "object",
1163 "properties": {
1164 "age": { "type": "integer" }
1165 }
1166 }
1167 }
1168 });
1169 let instance = serde_json::json!({ "user": { "age": "not-a-number" } });
1171 let errors = v.validate(&schema, &instance);
1172 assert!(!errors.is_empty());
1173 assert!(errors.iter().any(|e| e.path.contains("age")));
1174 }
1175
1176 #[test]
1177 fn test_validation_error_fields() {
1178 let err = ValidationError::new("/name", "required property 'name' is missing", "/required");
1179 assert_eq!(err.path, "/name");
1180 assert!(err.message.contains("name"));
1181 assert_eq!(err.schema_path, "/required");
1182 }
1183
1184 #[test]
1185 fn test_validator_no_errors_for_valid_complex_object() {
1186 let v = JsonSchemaValidator::new();
1187 let schema = serde_json::json!({
1188 "type": "object",
1189 "required": ["id", "name"],
1190 "properties": {
1191 "id": { "type": "integer" },
1192 "name": { "type": "string", "minLength": 1 },
1193 "score": { "type": "number", "minimum": 0.0, "maximum": 100.0 }
1194 },
1195 "additionalProperties": false
1196 });
1197 let instance = serde_json::json!({
1198 "id": 42,
1199 "name": "Alice",
1200 "score": 95.5
1201 });
1202 let errors = v.validate(&schema, &instance);
1203 assert!(errors.is_empty(), "unexpected errors: {:?}", errors);
1204 }
1205}