1use std::collections::{HashMap, HashSet};
6
7use serde_json::Value;
8
9use crate::error_codes::SchemaErrorCode;
10use crate::json_source_locator::JsonSourceLocator;
11use crate::types::{
12 is_valid_type, JsonLocation, SchemaValidatorOptions, ValidationError, ValidationResult,
13 COMPOSITION_KEYWORDS, KNOWN_EXTENSIONS, VALIDATION_EXTENSION_KEYWORDS,
14};
15
16pub struct SchemaValidator {
28 options: SchemaValidatorOptions,
29 #[allow(dead_code)]
30 external_schemas: HashMap<String, Value>,
31}
32
33impl Default for SchemaValidator {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl SchemaValidator {
40 #[must_use]
42 pub fn new() -> Self {
43 Self::with_options(SchemaValidatorOptions::default())
44 }
45
46 #[must_use]
48 pub fn with_options(options: SchemaValidatorOptions) -> Self {
49 let mut external_schemas = HashMap::new();
50 for schema in &options.external_schemas {
51 if let Some(id) = schema.get("$id").and_then(Value::as_str) {
52 external_schemas.insert(id.to_string(), schema.clone());
53 }
54 }
55 Self {
56 options,
57 external_schemas,
58 }
59 }
60
61 pub fn set_extended(&mut self, _extended: bool) {
64 }
66
67 pub fn is_extended(&self) -> bool {
70 true }
72
73 pub fn set_warn_on_extension_keywords(&mut self, warn: bool) {
75 self.options.warn_on_unused_extension_keywords = warn;
76 }
77
78 pub fn is_warn_on_extension_keywords(&self) -> bool {
80 self.options.warn_on_unused_extension_keywords
81 }
82
83 #[must_use]
87 pub fn validate(&self, schema_json: &str) -> ValidationResult {
88 let mut result = ValidationResult::new();
89 let locator = JsonSourceLocator::new(schema_json);
90
91 match serde_json::from_str::<Value>(schema_json) {
92 Ok(schema) => {
93 self.validate_schema_internal(&schema, &schema, &locator, &mut result, "", true, &mut HashSet::new(), 0);
94 }
95 Err(e) => {
96 result.add_error(ValidationError::schema_error(
97 SchemaErrorCode::SchemaInvalidType,
98 format!("Failed to parse JSON: {}", e),
99 "",
100 JsonLocation::unknown(),
101 ));
102 }
103 }
104
105 result
106 }
107
108 #[must_use]
112 pub fn validate_value(&self, schema: &Value, schema_json: &str) -> ValidationResult {
113 let mut result = ValidationResult::new();
114 let locator = JsonSourceLocator::new(schema_json);
115 self.validate_schema_internal(schema, schema, &locator, &mut result, "", true, &mut HashSet::new(), 0);
116 result
117 }
118
119 fn validate_schema(
121 &self,
122 schema: &Value,
123 root_schema: &Value,
124 locator: &JsonSourceLocator,
125 result: &mut ValidationResult,
126 path: &str,
127 is_root: bool,
128 visited_refs: &mut HashSet<String>,
129 depth: usize,
130 ) {
131 self.validate_schema_internal(schema, root_schema, locator, result, path, is_root, visited_refs, depth);
133 }
134
135 fn validate_schema_internal(
137 &self,
138 schema: &Value,
139 root_schema: &Value,
140 locator: &JsonSourceLocator,
141 result: &mut ValidationResult,
142 path: &str,
143 is_root: bool,
144 visited_refs: &mut HashSet<String>,
145 depth: usize,
146 ) {
147 if depth > self.options.max_validation_depth {
148 return;
149 }
150
151 match schema {
153 Value::Null => {
154 result.add_error(ValidationError::schema_error(
155 SchemaErrorCode::SchemaNull,
156 "Schema cannot be null",
157 path,
158 locator.get_location(path),
159 ));
160 return;
161 }
162 Value::Bool(_) => {
163 return;
165 }
166 Value::Object(_obj) => {
167 }
169 _ => {
170 result.add_error(ValidationError::schema_error(
171 SchemaErrorCode::SchemaInvalidType,
172 "Schema must be an object or boolean",
173 path,
174 locator.get_location(path),
175 ));
176 return;
177 }
178 }
179
180 let obj = schema.as_object().unwrap();
181
182 let mut enabled_extensions = HashSet::new();
184 if let Some(root_obj) = root_schema.as_object() {
185 if let Some(Value::Array(arr)) = root_obj.get("$uses") {
186 for ext in arr {
187 if let Value::String(s) = ext {
188 enabled_extensions.insert(s.as_str());
189 }
190 }
191 }
192 }
193
194 if is_root {
196 self.validate_root_schema(obj, locator, result, path);
197 }
198
199 if let Some(ref_val) = obj.get("$ref") {
201 self.validate_ref(ref_val, schema, root_schema, locator, result, path, visited_refs, depth);
202 }
203
204 if let Some(type_val) = obj.get("type") {
206 self.validate_type(type_val, obj, root_schema, locator, result, path, &enabled_extensions, visited_refs, depth);
207 }
208
209 if let Some(extends_val) = obj.get("$extends") {
211 self.validate_extends(extends_val, root_schema, locator, result, path, visited_refs, depth);
212 }
213
214 if let Some(altnames_val) = obj.get("altnames") {
216 self.validate_altnames(altnames_val, locator, result, path);
217 }
218
219 if let Some(defs) = obj.get("definitions") {
221 self.validate_definitions(defs, root_schema, locator, result, path, visited_refs, depth);
222 }
223
224 if let Some(enum_val) = obj.get("enum") {
226 self.validate_enum(enum_val, locator, result, path);
227 }
228
229 self.validate_composition(obj, root_schema, locator, result, path, &enabled_extensions, visited_refs, depth);
231
232 if self.options.warn_on_unused_extension_keywords {
234 self.check_extension_keywords(obj, locator, result, path, &enabled_extensions);
235 }
236 }
237
238 fn validate_root_schema(
240 &self,
241 obj: &serde_json::Map<String, Value>,
242 locator: &JsonSourceLocator,
243 result: &mut ValidationResult,
244 path: &str,
245 ) {
246 if !obj.contains_key("$id") {
248 result.add_error(ValidationError::schema_error(
249 SchemaErrorCode::SchemaRootMissingId,
250 "Root schema must have $id",
251 path,
252 locator.get_location(path),
253 ));
254 } else if let Some(id) = obj.get("$id") {
255 if !id.is_string() {
256 result.add_error(ValidationError::schema_error(
257 SchemaErrorCode::SchemaRootMissingId,
258 "$id must be a string",
259 &format!("{}/$id", path),
260 locator.get_location(&format!("{}/$id", path)),
261 ));
262 }
263 }
264
265 if obj.contains_key("type") && !obj.contains_key("name") {
267 result.add_error(ValidationError::schema_error(
268 SchemaErrorCode::SchemaRootMissingName,
269 "Root schema with type must have name",
270 path,
271 locator.get_location(path),
272 ));
273 }
274
275 let has_type = obj.contains_key("type");
277 let has_root = obj.contains_key("$root");
278 let has_definitions = obj.contains_key("definitions");
279 let has_composition = obj.keys().any(|k|
280 ["allOf", "anyOf", "oneOf", "not", "if"].contains(&k.as_str())
281 );
282
283 if !has_type && !has_root && !has_composition {
284 let has_only_meta = obj.keys().all(|k| {
286 k.starts_with('$') || k == "definitions" || k == "name" || k == "description"
287 });
288
289 if !has_only_meta || !has_definitions {
290 result.add_error(ValidationError::schema_error(
291 SchemaErrorCode::SchemaRootMissingType,
292 "Schema must have a 'type' property or '$root' reference",
293 path,
294 locator.get_location(path),
295 ));
296 }
297 }
298
299 if let Some(uses) = obj.get("$uses") {
301 self.validate_uses(uses, locator, result, path);
302 }
303
304 if let Some(offers) = obj.get("$offers") {
306 self.validate_offers(offers, locator, result, path);
307 }
308 }
309
310 fn validate_uses(
312 &self,
313 uses: &Value,
314 locator: &JsonSourceLocator,
315 result: &mut ValidationResult,
316 path: &str,
317 ) {
318 let uses_path = format!("{}/$uses", path);
319 match uses {
320 Value::Array(arr) => {
321 for (i, ext) in arr.iter().enumerate() {
322 if let Value::String(s) = ext {
323 if !KNOWN_EXTENSIONS.contains(&s.as_str()) {
324 result.add_error(ValidationError::schema_warning(
325 SchemaErrorCode::SchemaUsesInvalidExtension,
326 format!("Unknown extension: {}", s),
327 &format!("{}/{}", uses_path, i),
328 locator.get_location(&format!("{}/{}", uses_path, i)),
329 ));
330 }
331 } else {
332 result.add_error(ValidationError::schema_error(
333 SchemaErrorCode::SchemaUsesInvalidExtension,
334 "Extension name must be a string",
335 &format!("{}/{}", uses_path, i),
336 locator.get_location(&format!("{}/{}", uses_path, i)),
337 ));
338 }
339 }
340 }
341 _ => {
342 result.add_error(ValidationError::schema_error(
343 SchemaErrorCode::SchemaUsesNotArray,
344 "$uses must be an array",
345 &uses_path,
346 locator.get_location(&uses_path),
347 ));
348 }
349 }
350 }
351
352 fn validate_offers(
354 &self,
355 offers: &Value,
356 locator: &JsonSourceLocator,
357 result: &mut ValidationResult,
358 path: &str,
359 ) {
360 let offers_path = format!("{}/$offers", path);
361 match offers {
362 Value::Array(arr) => {
363 for (i, ext) in arr.iter().enumerate() {
364 if !ext.is_string() {
365 result.add_error(ValidationError::schema_error(
366 SchemaErrorCode::SchemaOffersInvalidExtension,
367 "Extension name must be a string",
368 &format!("{}/{}", offers_path, i),
369 locator.get_location(&format!("{}/{}", offers_path, i)),
370 ));
371 }
372 }
373 }
374 _ => {
375 result.add_error(ValidationError::schema_error(
376 SchemaErrorCode::SchemaOffersNotArray,
377 "$offers must be an array",
378 &offers_path,
379 locator.get_location(&offers_path),
380 ));
381 }
382 }
383 }
384
385 fn validate_ref(
387 &self,
388 ref_val: &Value,
389 _schema: &Value,
390 root_schema: &Value,
391 locator: &JsonSourceLocator,
392 result: &mut ValidationResult,
393 path: &str,
394 visited_refs: &mut HashSet<String>,
395 _depth: usize,
396 ) {
397 let ref_path = format!("{}/$ref", path);
398
399 match ref_val {
400 Value::String(ref_str) => {
401 if visited_refs.contains(ref_str) {
403 result.add_error(ValidationError::schema_error(
404 SchemaErrorCode::SchemaRefCircular,
405 format!("Circular reference detected: {}", ref_str),
406 &ref_path,
407 locator.get_location(&ref_path),
408 ));
409 return;
410 }
411
412 if ref_str.starts_with("#/definitions/") {
414 if let Some(resolved) = self.resolve_ref(ref_str, root_schema) {
416 if let Value::Object(def_obj) = resolved {
418 let keys: Vec<&String> = def_obj.keys().collect();
419 let is_bare_ref = keys.len() == 1 && keys[0] == "$ref";
420 let is_type_ref_only = keys.len() == 1 && keys[0] == "type" && {
421 if let Some(Value::Object(type_obj)) = def_obj.get("type") {
422 type_obj.len() == 1 && type_obj.contains_key("$ref")
423 } else {
424 false
425 }
426 };
427
428 if is_bare_ref || is_type_ref_only {
429 let inner_ref = if is_bare_ref {
431 def_obj.get("$ref").and_then(|v| v.as_str())
432 } else if is_type_ref_only {
433 def_obj.get("type")
434 .and_then(|t| t.as_object())
435 .and_then(|o| o.get("$ref"))
436 .and_then(|v| v.as_str())
437 } else {
438 None
439 };
440
441 if inner_ref == Some(ref_str) {
442 result.add_error(ValidationError::schema_error(
443 SchemaErrorCode::SchemaRefCircular,
444 format!("Direct circular reference: {}", ref_str),
445 &ref_path,
446 locator.get_location(&ref_path),
447 ));
448 }
449 }
450 }
451 } else {
452 result.add_error(ValidationError::schema_error(
453 SchemaErrorCode::SchemaRefNotFound,
454 format!("Reference not found: {}", ref_str),
455 &ref_path,
456 locator.get_location(&ref_path),
457 ));
458 }
459 }
460 }
462 _ => {
463 result.add_error(ValidationError::schema_error(
464 SchemaErrorCode::SchemaRefNotString,
465 "$ref must be a string",
466 &ref_path,
467 locator.get_location(&ref_path),
468 ));
469 }
470 }
471 }
472
473 fn resolve_ref<'a>(&self, ref_str: &str, root_schema: &'a Value) -> Option<&'a Value> {
475 if !ref_str.starts_with("#/") {
476 return None;
477 }
478
479 let path_parts: Vec<&str> = ref_str[2..].split('/').collect();
480 let mut current = root_schema;
481
482 for part in path_parts {
483 let unescaped = part.replace("~1", "/").replace("~0", "~");
485 current = current.get(&unescaped)?;
486 }
487
488 Some(current)
489 }
490
491 fn validate_type(
493 &self,
494 type_val: &Value,
495 obj: &serde_json::Map<String, Value>,
496 root_schema: &Value,
497 locator: &JsonSourceLocator,
498 result: &mut ValidationResult,
499 path: &str,
500 enabled_extensions: &HashSet<&str>,
501 visited_refs: &mut HashSet<String>,
502 depth: usize,
503 ) {
504 let type_path = format!("{}/type", path);
505
506 match type_val {
507 Value::String(type_name) => {
508 self.validate_single_type(type_name, obj, root_schema, locator, result, path, enabled_extensions, depth);
509 }
510 Value::Array(types) => {
511 if types.is_empty() {
513 result.add_error(ValidationError::schema_error(
514 SchemaErrorCode::SchemaTypeArrayEmpty,
515 "Union type array cannot be empty",
516 &type_path,
517 locator.get_location(&type_path),
518 ));
519 return;
520 }
521 for (i, t) in types.iter().enumerate() {
522 let elem_path = format!("{}/{}", type_path, i);
523 match t {
524 Value::String(s) => {
525 if !is_valid_type(s) {
526 result.add_error(ValidationError::schema_error(
527 SchemaErrorCode::SchemaTypeInvalid,
528 format!("Unknown type in union: '{}'", s),
529 &elem_path,
530 locator.get_location(&elem_path),
531 ));
532 }
533 }
534 Value::Object(ref_obj) => {
535 if let Some(ref_val) = ref_obj.get("$ref") {
536 if let Value::String(ref_str) = ref_val {
537 self.validate_type_ref(ref_str, root_schema, locator, result, &elem_path, visited_refs);
539 } else {
540 result.add_error(ValidationError::schema_error(
541 SchemaErrorCode::SchemaRefNotString,
542 "$ref must be a string",
543 &format!("{}/$ref", elem_path),
544 locator.get_location(&format!("{}/$ref", elem_path)),
545 ));
546 }
547 } else {
548 result.add_error(ValidationError::schema_error(
549 SchemaErrorCode::SchemaTypeObjectMissingRef,
550 "Union type object must have $ref",
551 &elem_path,
552 locator.get_location(&elem_path),
553 ));
554 }
555 }
556 _ => {
557 result.add_error(ValidationError::schema_error(
558 SchemaErrorCode::SchemaKeywordInvalidType,
559 "Union type elements must be strings or $ref objects",
560 &elem_path,
561 locator.get_location(&elem_path),
562 ));
563 }
564 }
565 }
566 }
567 Value::Object(ref_obj) => {
568 if let Some(ref_val) = ref_obj.get("$ref") {
570 if let Value::String(ref_str) = ref_val {
571 self.validate_type_ref(ref_str, root_schema, locator, result, path, visited_refs);
573 } else {
574 result.add_error(ValidationError::schema_error(
575 SchemaErrorCode::SchemaRefNotString,
576 "$ref must be a string",
577 &format!("{}/$ref", type_path),
578 locator.get_location(&format!("{}/$ref", type_path)),
579 ));
580 }
581 } else {
582 result.add_error(ValidationError::schema_error(
583 SchemaErrorCode::SchemaTypeObjectMissingRef,
584 "type object must have $ref",
585 &type_path,
586 locator.get_location(&type_path),
587 ));
588 }
589 }
590 _ => {
591 result.add_error(ValidationError::schema_error(
592 SchemaErrorCode::SchemaKeywordInvalidType,
593 "type must be a string, array, or object with $ref",
594 &type_path,
595 locator.get_location(&type_path),
596 ));
597 }
598 }
599 }
600
601 fn validate_type_ref(
603 &self,
604 ref_str: &str,
605 root_schema: &Value,
606 locator: &JsonSourceLocator,
607 result: &mut ValidationResult,
608 path: &str,
609 visited_refs: &mut HashSet<String>,
610 ) {
611 let ref_path = format!("{}/type/$ref", path);
612
613 if ref_str.starts_with("#/definitions/") {
614 if visited_refs.contains(ref_str) {
616 if let Some(resolved) = self.resolve_ref(ref_str, root_schema) {
618 if let Value::Object(def_obj) = resolved {
619 let keys: Vec<&String> = def_obj.keys().collect();
620 let is_type_ref_only = keys.len() == 1 && keys[0] == "type" && {
621 if let Some(Value::Object(type_obj)) = def_obj.get("type") {
622 type_obj.len() == 1 && type_obj.contains_key("$ref")
623 } else {
624 false
625 }
626 };
627
628 if is_type_ref_only {
629 result.add_error(ValidationError::schema_error(
630 SchemaErrorCode::SchemaRefCircular,
631 format!("Direct circular reference: {}", ref_str),
632 &ref_path,
633 locator.get_location(&ref_path),
634 ));
635 }
636 }
637 }
638 return;
639 }
640
641 visited_refs.insert(ref_str.to_string());
643
644 if let Some(resolved) = self.resolve_ref(ref_str, root_schema) {
646 if let Value::Object(def_obj) = resolved {
648 if let Some(type_val) = def_obj.get("type") {
649 if let Value::Object(type_obj) = type_val {
650 if let Some(Value::String(inner_ref)) = type_obj.get("$ref") {
651 self.validate_type_ref(inner_ref, root_schema, locator, result, path, visited_refs);
652 }
653 }
654 }
655 }
656 } else {
657 result.add_error(ValidationError::schema_error(
658 SchemaErrorCode::SchemaRefNotFound,
659 format!("Reference not found: {}", ref_str),
660 &ref_path,
661 locator.get_location(&ref_path),
662 ));
663 }
664
665 visited_refs.remove(ref_str);
666 }
667 }
668
669 fn validate_single_type(
671 &self,
672 type_name: &str,
673 obj: &serde_json::Map<String, Value>,
674 root_schema: &Value,
675 locator: &JsonSourceLocator,
676 result: &mut ValidationResult,
677 path: &str,
678 enabled_extensions: &HashSet<&str>,
679 depth: usize,
680 ) {
681 let type_path = format!("{}/type", path);
682
683 if !is_valid_type(type_name) {
685 result.add_error(ValidationError::schema_error(
686 SchemaErrorCode::SchemaTypeInvalid,
687 format!("Invalid type: {}", type_name),
688 &type_path,
689 locator.get_location(&type_path),
690 ));
691 return;
692 }
693
694 match type_name {
696 "object" => self.validate_object_type(obj, root_schema, locator, result, path, enabled_extensions, depth),
697 "array" | "set" => self.validate_array_type(obj, root_schema, locator, result, path, type_name),
698 "map" => self.validate_map_type(obj, root_schema, locator, result, path),
699 "tuple" => self.validate_tuple_type(obj, root_schema, locator, result, path),
700 "choice" => self.validate_choice_type(obj, root_schema, locator, result, path),
701 _ => {
702 self.validate_primitive_constraints(type_name, obj, locator, result, path);
704 }
705 }
706 }
707
708 fn validate_primitive_constraints(
710 &self,
711 type_name: &str,
712 obj: &serde_json::Map<String, Value>,
713 locator: &JsonSourceLocator,
714 result: &mut ValidationResult,
715 path: &str,
716 ) {
717 use crate::types::is_numeric_type;
718
719 let is_numeric = is_numeric_type(type_name);
721 let is_string = type_name == "string";
722
723 if obj.contains_key("minimum") && !is_numeric {
725 result.add_error(ValidationError::schema_error(
726 SchemaErrorCode::SchemaConstraintTypeMismatch,
727 format!("minimum constraint cannot be used with type '{}'", type_name),
728 &format!("{}/minimum", path),
729 locator.get_location(&format!("{}/minimum", path)),
730 ));
731 }
732 if obj.contains_key("maximum") && !is_numeric {
733 result.add_error(ValidationError::schema_error(
734 SchemaErrorCode::SchemaConstraintTypeMismatch,
735 format!("maximum constraint cannot be used with type '{}'", type_name),
736 &format!("{}/maximum", path),
737 locator.get_location(&format!("{}/maximum", path)),
738 ));
739 }
740
741 if obj.contains_key("minLength") && !is_string {
743 result.add_error(ValidationError::schema_error(
744 SchemaErrorCode::SchemaConstraintTypeMismatch,
745 format!("minLength constraint cannot be used with type '{}'", type_name),
746 &format!("{}/minLength", path),
747 locator.get_location(&format!("{}/minLength", path)),
748 ));
749 }
750 if obj.contains_key("maxLength") && !is_string {
751 result.add_error(ValidationError::schema_error(
752 SchemaErrorCode::SchemaConstraintTypeMismatch,
753 format!("maxLength constraint cannot be used with type '{}'", type_name),
754 &format!("{}/maxLength", path),
755 locator.get_location(&format!("{}/maxLength", path)),
756 ));
757 }
758
759 if is_numeric {
761 self.validate_numeric_constraints(obj, locator, result, path);
762 }
763
764 if is_string {
766 self.validate_string_constraints(obj, locator, result, path);
767 }
768
769 if let Some(multiple_of) = obj.get("multipleOf") {
771 if !is_numeric {
772 result.add_error(ValidationError::schema_error(
773 SchemaErrorCode::SchemaConstraintTypeMismatch,
774 format!("multipleOf constraint cannot be used with type '{}'", type_name),
775 &format!("{}/multipleOf", path),
776 locator.get_location(&format!("{}/multipleOf", path)),
777 ));
778 } else if let Some(n) = multiple_of.as_f64() {
779 if n <= 0.0 {
780 result.add_error(ValidationError::schema_error(
781 SchemaErrorCode::SchemaMultipleOfInvalid,
782 "multipleOf must be greater than 0",
783 &format!("{}/multipleOf", path),
784 locator.get_location(&format!("{}/multipleOf", path)),
785 ));
786 }
787 }
788 }
789
790 if let Some(Value::String(pattern)) = obj.get("pattern") {
792 if regex::Regex::new(pattern).is_err() {
793 result.add_error(ValidationError::schema_error(
794 SchemaErrorCode::SchemaPatternInvalid,
795 format!("Invalid regular expression pattern: {}", pattern),
796 &format!("{}/pattern", path),
797 locator.get_location(&format!("{}/pattern", path)),
798 ));
799 }
800 }
801 }
802
803 fn validate_numeric_constraints(
805 &self,
806 obj: &serde_json::Map<String, Value>,
807 locator: &JsonSourceLocator,
808 result: &mut ValidationResult,
809 path: &str,
810 ) {
811 let minimum = obj.get("minimum").and_then(Value::as_f64);
812 let maximum = obj.get("maximum").and_then(Value::as_f64);
813
814 if let (Some(min), Some(max)) = (minimum, maximum) {
815 if min > max {
816 result.add_error(ValidationError::schema_error(
817 SchemaErrorCode::SchemaMinimumExceedsMaximum,
818 format!("minimum ({}) exceeds maximum ({})", min, max),
819 &format!("{}/minimum", path),
820 locator.get_location(&format!("{}/minimum", path)),
821 ));
822 }
823 }
824 }
825
826 fn validate_string_constraints(
828 &self,
829 obj: &serde_json::Map<String, Value>,
830 locator: &JsonSourceLocator,
831 result: &mut ValidationResult,
832 path: &str,
833 ) {
834 let min_length = obj.get("minLength").and_then(Value::as_i64);
835 let max_length = obj.get("maxLength").and_then(Value::as_i64);
836
837 if let Some(min) = min_length {
838 if min < 0 {
839 result.add_error(ValidationError::schema_error(
840 SchemaErrorCode::SchemaMinLengthNegative,
841 "minLength cannot be negative",
842 &format!("{}/minLength", path),
843 locator.get_location(&format!("{}/minLength", path)),
844 ));
845 }
846 }
847
848 if let Some(max) = max_length {
849 if max < 0 {
850 result.add_error(ValidationError::schema_error(
851 SchemaErrorCode::SchemaMaxLengthNegative,
852 "maxLength cannot be negative",
853 &format!("{}/maxLength", path),
854 locator.get_location(&format!("{}/maxLength", path)),
855 ));
856 }
857 }
858
859 if let (Some(min), Some(max)) = (min_length, max_length) {
860 if min > max {
861 result.add_error(ValidationError::schema_error(
862 SchemaErrorCode::SchemaMinLengthExceedsMaxLength,
863 format!("minLength ({}) exceeds maxLength ({})", min, max),
864 &format!("{}/minLength", path),
865 locator.get_location(&format!("{}/minLength", path)),
866 ));
867 }
868 }
869 }
870
871 fn validate_object_type(
873 &self,
874 obj: &serde_json::Map<String, Value>,
875 root_schema: &Value,
876 locator: &JsonSourceLocator,
877 result: &mut ValidationResult,
878 path: &str,
879 _enabled_extensions: &HashSet<&str>,
880 depth: usize,
881 ) {
882 if let Some(props) = obj.get("properties") {
884 let props_path = format!("{}/properties", path);
885 match props {
886 Value::Object(props_obj) => {
887 for (prop_name, prop_schema) in props_obj {
888 let prop_path = format!("{}/{}", props_path, prop_name);
889 self.validate_schema(
890 prop_schema,
891 root_schema,
892 locator,
893 result,
894 &prop_path,
895 false,
896 &mut HashSet::new(),
897 depth + 1,
898 );
899 }
900 }
901 _ => {
902 result.add_error(ValidationError::schema_error(
903 SchemaErrorCode::SchemaPropertiesMustBeObject,
904 "properties must be an object",
905 &props_path,
906 locator.get_location(&props_path),
907 ));
908 }
909 }
910 }
911
912 if let Some(required) = obj.get("required") {
914 self.validate_required(required, obj.get("properties"), locator, result, path);
915 }
916
917 if let Some(additional) = obj.get("additionalProperties") {
919 let add_path = format!("{}/additionalProperties", path);
920 match additional {
921 Value::Bool(_) => {}
922 Value::Object(_) => {
923 self.validate_schema(
924 additional,
925 root_schema,
926 locator,
927 result,
928 &add_path,
929 false,
930 &mut HashSet::new(),
931 depth + 1,
932 );
933 }
934 _ => {
935 result.add_error(ValidationError::schema_error(
936 SchemaErrorCode::SchemaAdditionalPropertiesInvalid,
937 "additionalProperties must be a boolean or schema object",
938 &add_path,
939 locator.get_location(&add_path),
940 ));
941 }
942 }
943 }
944 }
945
946 fn validate_required(
948 &self,
949 required: &Value,
950 properties: Option<&Value>,
951 locator: &JsonSourceLocator,
952 result: &mut ValidationResult,
953 path: &str,
954 ) {
955 let required_path = format!("{}/required", path);
956
957 match required {
958 Value::Array(arr) => {
959 let mut seen = HashSet::new();
960 for (i, item) in arr.iter().enumerate() {
961 match item {
962 Value::String(s) => {
963 if !seen.insert(s.clone()) {
965 result.add_error(ValidationError::schema_warning(
966 SchemaErrorCode::SchemaRequiredPropertyNotDefined,
967 format!("Duplicate required property: {}", s),
968 &format!("{}/{}", required_path, i),
969 locator.get_location(&format!("{}/{}", required_path, i)),
970 ));
971 }
972
973 if let Some(Value::Object(props)) = properties {
975 if !props.contains_key(s) {
976 result.add_error(ValidationError::schema_error(
977 SchemaErrorCode::SchemaRequiredPropertyNotDefined,
978 format!("Required property not defined: {}", s),
979 &format!("{}/{}", required_path, i),
980 locator.get_location(&format!("{}/{}", required_path, i)),
981 ));
982 }
983 }
984 }
985 _ => {
986 result.add_error(ValidationError::schema_error(
987 SchemaErrorCode::SchemaRequiredItemMustBeString,
988 "Required item must be a string",
989 &format!("{}/{}", required_path, i),
990 locator.get_location(&format!("{}/{}", required_path, i)),
991 ));
992 }
993 }
994 }
995 }
996 _ => {
997 result.add_error(ValidationError::schema_error(
998 SchemaErrorCode::SchemaRequiredMustBeArray,
999 "required must be an array",
1000 &required_path,
1001 locator.get_location(&required_path),
1002 ));
1003 }
1004 }
1005 }
1006
1007 fn validate_array_type(
1009 &self,
1010 obj: &serde_json::Map<String, Value>,
1011 root_schema: &Value,
1012 locator: &JsonSourceLocator,
1013 result: &mut ValidationResult,
1014 path: &str,
1015 type_name: &str,
1016 ) {
1017 if !obj.contains_key("items") {
1019 result.add_error(ValidationError::schema_error(
1020 SchemaErrorCode::SchemaArrayMissingItems,
1021 format!("{} type requires items keyword", type_name),
1022 path,
1023 locator.get_location(path),
1024 ));
1025 } else if let Some(items) = obj.get("items") {
1026 let items_path = format!("{}/items", path);
1027 self.validate_schema(items, root_schema, locator, result, &items_path, false, &mut HashSet::new(), 0);
1028 }
1029
1030 let min_items = obj.get("minItems").and_then(Value::as_i64);
1032 let max_items = obj.get("maxItems").and_then(Value::as_i64);
1033
1034 if let Some(min) = min_items {
1035 if min < 0 {
1036 result.add_error(ValidationError::schema_error(
1037 SchemaErrorCode::SchemaMinItemsNegative,
1038 "minItems cannot be negative",
1039 &format!("{}/minItems", path),
1040 locator.get_location(&format!("{}/minItems", path)),
1041 ));
1042 }
1043 }
1044
1045 if let (Some(min), Some(max)) = (min_items, max_items) {
1046 if min > max {
1047 result.add_error(ValidationError::schema_error(
1048 SchemaErrorCode::SchemaMinItemsExceedsMaxItems,
1049 format!("minItems ({}) exceeds maxItems ({})", min, max),
1050 &format!("{}/minItems", path),
1051 locator.get_location(&format!("{}/minItems", path)),
1052 ));
1053 }
1054 }
1055 }
1056
1057 fn validate_map_type(
1059 &self,
1060 obj: &serde_json::Map<String, Value>,
1061 root_schema: &Value,
1062 locator: &JsonSourceLocator,
1063 result: &mut ValidationResult,
1064 path: &str,
1065 ) {
1066 if !obj.contains_key("values") {
1068 result.add_error(ValidationError::schema_error(
1069 SchemaErrorCode::SchemaMapMissingValues,
1070 "map type requires values keyword",
1071 path,
1072 locator.get_location(path),
1073 ));
1074 } else if let Some(values) = obj.get("values") {
1075 let values_path = format!("{}/values", path);
1076 self.validate_schema(values, root_schema, locator, result, &values_path, false, &mut HashSet::new(), 0);
1077 }
1078 }
1079
1080 fn validate_tuple_type(
1082 &self,
1083 obj: &serde_json::Map<String, Value>,
1084 _root_schema: &Value,
1085 locator: &JsonSourceLocator,
1086 result: &mut ValidationResult,
1087 path: &str,
1088 ) {
1089 let has_properties = obj.contains_key("properties");
1091 let has_tuple = obj.contains_key("tuple");
1092
1093 if !has_properties || !has_tuple {
1094 result.add_error(ValidationError::schema_error(
1095 SchemaErrorCode::SchemaTupleMissingDefinition,
1096 "tuple type requires both properties and tuple keywords",
1097 path,
1098 locator.get_location(path),
1099 ));
1100 return;
1101 }
1102
1103 if let Some(tuple) = obj.get("tuple") {
1105 let tuple_path = format!("{}/tuple", path);
1106 match tuple {
1107 Value::Array(arr) => {
1108 let properties = obj.get("properties").and_then(Value::as_object);
1109
1110 for (i, item) in arr.iter().enumerate() {
1111 match item {
1112 Value::String(s) => {
1113 if let Some(props) = properties {
1115 if !props.contains_key(s) {
1116 result.add_error(ValidationError::schema_error(
1117 SchemaErrorCode::SchemaTuplePropertyNotDefined,
1118 format!("Tuple element references undefined property: {}", s),
1119 &format!("{}/{}", tuple_path, i),
1120 locator.get_location(&format!("{}/{}", tuple_path, i)),
1121 ));
1122 }
1123 }
1124 }
1125 _ => {
1126 result.add_error(ValidationError::schema_error(
1127 SchemaErrorCode::SchemaTupleInvalidFormat,
1128 "Tuple element must be a string (property name)",
1129 &format!("{}/{}", tuple_path, i),
1130 locator.get_location(&format!("{}/{}", tuple_path, i)),
1131 ));
1132 }
1133 }
1134 }
1135 }
1136 _ => {
1137 result.add_error(ValidationError::schema_error(
1138 SchemaErrorCode::SchemaTupleInvalidFormat,
1139 "tuple must be an array",
1140 &tuple_path,
1141 locator.get_location(&tuple_path),
1142 ));
1143 }
1144 }
1145 }
1146 }
1147
1148 fn validate_choice_type(
1150 &self,
1151 obj: &serde_json::Map<String, Value>,
1152 root_schema: &Value,
1153 locator: &JsonSourceLocator,
1154 result: &mut ValidationResult,
1155 path: &str,
1156 ) {
1157 if !obj.contains_key("choices") {
1159 result.add_error(ValidationError::schema_error(
1160 SchemaErrorCode::SchemaChoiceMissingChoices,
1161 "choice type requires choices keyword",
1162 path,
1163 locator.get_location(path),
1164 ));
1165 return;
1166 }
1167
1168 if let Some(choices) = obj.get("choices") {
1170 let choices_path = format!("{}/choices", path);
1171 match choices {
1172 Value::Object(choices_obj) => {
1173 for (choice_name, choice_schema) in choices_obj {
1174 let choice_path = format!("{}/{}", choices_path, choice_name);
1175 self.validate_schema(
1176 choice_schema,
1177 root_schema,
1178 locator,
1179 result,
1180 &choice_path,
1181 false,
1182 &mut HashSet::new(),
1183 0,
1184 );
1185 }
1186 }
1187 _ => {
1188 result.add_error(ValidationError::schema_error(
1189 SchemaErrorCode::SchemaChoicesNotObject,
1190 "choices must be an object",
1191 &choices_path,
1192 locator.get_location(&choices_path),
1193 ));
1194 }
1195 }
1196 }
1197
1198 if let Some(selector) = obj.get("selector") {
1200 let selector_path = format!("{}/selector", path);
1201 if !selector.is_string() {
1202 result.add_error(ValidationError::schema_error(
1203 SchemaErrorCode::SchemaSelectorNotString,
1204 "selector must be a string",
1205 &selector_path,
1206 locator.get_location(&selector_path),
1207 ));
1208 }
1209 }
1210 }
1211
1212 fn validate_definitions(
1214 &self,
1215 defs: &Value,
1216 root_schema: &Value,
1217 locator: &JsonSourceLocator,
1218 result: &mut ValidationResult,
1219 path: &str,
1220 visited_refs: &mut HashSet<String>,
1221 depth: usize,
1222 ) {
1223 let defs_path = format!("{}/definitions", path);
1224
1225 match defs {
1226 Value::Object(defs_obj) => {
1227 for (def_name, def_schema) in defs_obj {
1228 let def_path = format!("{}/{}", defs_path, def_name);
1229 self.validate_definition_or_namespace(def_schema, root_schema, locator, result, &def_path, visited_refs, depth);
1230 }
1231 }
1232 _ => {
1233 result.add_error(ValidationError::schema_error(
1234 SchemaErrorCode::SchemaDefinitionsMustBeObject,
1235 "definitions must be an object",
1236 &defs_path,
1237 locator.get_location(&defs_path),
1238 ));
1239 }
1240 }
1241 }
1242
1243 fn validate_definition_or_namespace(
1245 &self,
1246 def_schema: &Value,
1247 root_schema: &Value,
1248 locator: &JsonSourceLocator,
1249 result: &mut ValidationResult,
1250 path: &str,
1251 visited_refs: &mut HashSet<String>,
1252 depth: usize,
1253 ) {
1254 if let Value::Object(def_obj) = def_schema {
1255 if def_obj.is_empty() {
1257 result.add_error(ValidationError::schema_error(
1258 SchemaErrorCode::SchemaDefinitionMissingType,
1259 "Definition must have type, $ref, definitions, or composition",
1260 path,
1261 locator.get_location(path),
1262 ));
1263 return;
1264 }
1265
1266 let has_type = def_obj.contains_key("type");
1267 let has_ref = def_obj.contains_key("$ref");
1268 let has_definitions = def_obj.contains_key("definitions");
1269 let has_composition = def_obj.keys().any(|k|
1270 ["allOf", "anyOf", "oneOf", "not", "if"].contains(&k.as_str())
1271 );
1272
1273 if has_type || has_ref || has_definitions || has_composition {
1274 self.validate_schema_internal(
1276 def_schema,
1277 root_schema,
1278 locator,
1279 result,
1280 path,
1281 false,
1282 visited_refs,
1283 depth + 1,
1284 );
1285 } else {
1286 let is_namespace = def_obj.values().all(|v| {
1289 if let Value::Object(child) = v {
1290 child.contains_key("type")
1291 || child.contains_key("$ref")
1292 || child.contains_key("definitions")
1293 || child.keys().any(|k| ["allOf", "anyOf", "oneOf"].contains(&k.as_str()))
1294 || child.values().all(|cv| cv.is_object()) } else {
1296 false
1297 }
1298 });
1299
1300 if is_namespace {
1301 for (child_name, child_schema) in def_obj {
1303 let child_path = format!("{}/{}", path, child_name);
1304 self.validate_definition_or_namespace(child_schema, root_schema, locator, result, &child_path, visited_refs, depth + 1);
1305 }
1306 } else {
1307 result.add_error(ValidationError::schema_error(
1309 SchemaErrorCode::SchemaDefinitionMissingType,
1310 "Definition must have type, $ref, definitions, or composition",
1311 path,
1312 locator.get_location(path),
1313 ));
1314 }
1315 }
1316 } else {
1317 result.add_error(ValidationError::schema_error(
1318 SchemaErrorCode::SchemaDefinitionInvalid,
1319 "Definition must be an object",
1320 path,
1321 locator.get_location(path),
1322 ));
1323 }
1324 }
1325
1326 fn validate_enum(
1328 &self,
1329 enum_val: &Value,
1330 locator: &JsonSourceLocator,
1331 result: &mut ValidationResult,
1332 path: &str,
1333 ) {
1334 let enum_path = format!("{}/enum", path);
1335
1336 match enum_val {
1337 Value::Array(arr) => {
1338 if arr.is_empty() {
1339 result.add_error(ValidationError::schema_error(
1340 SchemaErrorCode::SchemaEnumEmpty,
1341 "enum cannot be empty",
1342 &enum_path,
1343 locator.get_location(&enum_path),
1344 ));
1345 return;
1346 }
1347
1348 let mut seen = Vec::new();
1350 for (i, item) in arr.iter().enumerate() {
1351 let item_str = item.to_string();
1352 if seen.contains(&item_str) {
1353 result.add_error(ValidationError::schema_error(
1354 SchemaErrorCode::SchemaEnumDuplicates,
1355 "enum contains duplicate values",
1356 &format!("{}/{}", enum_path, i),
1357 locator.get_location(&format!("{}/{}", enum_path, i)),
1358 ));
1359 } else {
1360 seen.push(item_str);
1361 }
1362 }
1363 }
1364 _ => {
1365 result.add_error(ValidationError::schema_error(
1366 SchemaErrorCode::SchemaEnumNotArray,
1367 "enum must be an array",
1368 &enum_path,
1369 locator.get_location(&enum_path),
1370 ));
1371 }
1372 }
1373 }
1374
1375 fn validate_composition(
1377 &self,
1378 obj: &serde_json::Map<String, Value>,
1379 root_schema: &Value,
1380 locator: &JsonSourceLocator,
1381 result: &mut ValidationResult,
1382 path: &str,
1383 _enabled_extensions: &HashSet<&str>,
1384 visited_refs: &mut HashSet<String>,
1385 depth: usize,
1386 ) {
1387 if let Some(all_of) = obj.get("allOf") {
1389 self.validate_composition_array(all_of, "allOf", root_schema, locator, result, path, visited_refs, depth);
1390 }
1391
1392 if let Some(any_of) = obj.get("anyOf") {
1394 self.validate_composition_array(any_of, "anyOf", root_schema, locator, result, path, visited_refs, depth);
1395 }
1396
1397 if let Some(one_of) = obj.get("oneOf") {
1399 self.validate_composition_array(one_of, "oneOf", root_schema, locator, result, path, visited_refs, depth);
1400 }
1401
1402 if let Some(not) = obj.get("not") {
1404 let not_path = format!("{}/not", path);
1405 self.validate_schema_internal(not, root_schema, locator, result, ¬_path, false, visited_refs, depth + 1);
1406 }
1407
1408 if let Some(if_schema) = obj.get("if") {
1410 let if_path = format!("{}/if", path);
1411 self.validate_schema_internal(if_schema, root_schema, locator, result, &if_path, false, visited_refs, depth + 1);
1412 }
1413
1414 if let Some(then_schema) = obj.get("then") {
1415 if !obj.contains_key("if") {
1416 result.add_error(ValidationError::schema_error(
1417 SchemaErrorCode::SchemaThenWithoutIf,
1418 "then requires if",
1419 &format!("{}/then", path),
1420 locator.get_location(&format!("{}/then", path)),
1421 ));
1422 }
1423 let then_path = format!("{}/then", path);
1424 self.validate_schema_internal(then_schema, root_schema, locator, result, &then_path, false, visited_refs, depth + 1);
1425 }
1426
1427 if let Some(else_schema) = obj.get("else") {
1428 if !obj.contains_key("if") {
1429 result.add_error(ValidationError::schema_error(
1430 SchemaErrorCode::SchemaElseWithoutIf,
1431 "else requires if",
1432 &format!("{}/else", path),
1433 locator.get_location(&format!("{}/else", path)),
1434 ));
1435 }
1436 let else_path = format!("{}/else", path);
1437 self.validate_schema_internal(else_schema, root_schema, locator, result, &else_path, false, visited_refs, depth + 1);
1438 }
1439 }
1440
1441 fn validate_composition_array(
1443 &self,
1444 value: &Value,
1445 keyword: &str,
1446 root_schema: &Value,
1447 locator: &JsonSourceLocator,
1448 result: &mut ValidationResult,
1449 path: &str,
1450 visited_refs: &mut HashSet<String>,
1451 depth: usize,
1452 ) {
1453 let keyword_path = format!("{}/{}", path, keyword);
1454
1455 match value {
1456 Value::Array(arr) => {
1457 if arr.is_empty() {
1459 result.add_error(ValidationError::schema_error(
1460 SchemaErrorCode::SchemaKeywordInvalidType,
1461 format!("{} array cannot be empty", keyword),
1462 &keyword_path,
1463 locator.get_location(&keyword_path),
1464 ));
1465 return;
1466 }
1467
1468 for (i, item) in arr.iter().enumerate() {
1469 let item_path = format!("{}/{}", keyword_path, i);
1470 self.validate_schema_internal(item, root_schema, locator, result, &item_path, false, visited_refs, depth + 1);
1471 }
1472 }
1473 _ => {
1474 let code = match keyword {
1475 "allOf" => SchemaErrorCode::SchemaAllOfNotArray,
1476 "anyOf" => SchemaErrorCode::SchemaAnyOfNotArray,
1477 "oneOf" => SchemaErrorCode::SchemaOneOfNotArray,
1478 _ => SchemaErrorCode::SchemaAllOfNotArray,
1479 };
1480 result.add_error(ValidationError::schema_error(
1481 code,
1482 format!("{} must be an array", keyword),
1483 &keyword_path,
1484 locator.get_location(&keyword_path),
1485 ));
1486 }
1487 }
1488 }
1489
1490 fn validate_extends(
1492 &self,
1493 extends_val: &Value,
1494 root_schema: &Value,
1495 locator: &JsonSourceLocator,
1496 result: &mut ValidationResult,
1497 path: &str,
1498 visited_refs: &mut HashSet<String>,
1499 _depth: usize,
1500 ) {
1501 let extends_path = format!("{}/$extends", path);
1502
1503 let refs: Vec<(String, String)> = match extends_val {
1505 Value::String(s) => {
1506 if s.is_empty() {
1507 result.add_error(ValidationError::schema_error(
1508 SchemaErrorCode::SchemaExtendsEmpty,
1509 "$extends cannot be empty",
1510 &extends_path,
1511 locator.get_location(&extends_path),
1512 ));
1513 return;
1514 }
1515 vec![(s.clone(), extends_path.clone())]
1516 }
1517 Value::Array(arr) => {
1518 if arr.is_empty() {
1519 result.add_error(ValidationError::schema_error(
1520 SchemaErrorCode::SchemaExtendsEmpty,
1521 "$extends array cannot be empty",
1522 &extends_path,
1523 locator.get_location(&extends_path),
1524 ));
1525 return;
1526 }
1527 let mut refs = Vec::new();
1528 for (i, item) in arr.iter().enumerate() {
1529 let item_path = format!("{}/{}", extends_path, i);
1530 if let Value::String(s) = item {
1531 if s.is_empty() {
1532 result.add_error(ValidationError::schema_error(
1533 SchemaErrorCode::SchemaExtendsEmpty,
1534 "$extends array items cannot be empty",
1535 &item_path,
1536 locator.get_location(&item_path),
1537 ));
1538 } else {
1539 refs.push((s.clone(), item_path));
1540 }
1541 } else {
1542 result.add_error(ValidationError::schema_error(
1543 SchemaErrorCode::SchemaExtendsNotString,
1544 "$extends array items must be strings",
1545 &item_path,
1546 locator.get_location(&item_path),
1547 ));
1548 }
1549 }
1550 refs
1551 }
1552 _ => {
1553 result.add_error(ValidationError::schema_error(
1554 SchemaErrorCode::SchemaExtendsNotString,
1555 "$extends must be a string or array of strings",
1556 &extends_path,
1557 locator.get_location(&extends_path),
1558 ));
1559 return;
1560 }
1561 };
1562
1563 for (ref_str, ref_path) in refs {
1565 if visited_refs.contains(&ref_str) {
1567 result.add_error(ValidationError::schema_error(
1568 SchemaErrorCode::SchemaExtendsCircular,
1569 format!("Circular $extends reference: {}", ref_str),
1570 &ref_path,
1571 locator.get_location(&ref_path),
1572 ));
1573 continue;
1574 }
1575
1576 if ref_str.starts_with("#/definitions/") && self.resolve_ref(&ref_str, root_schema).is_none() {
1578 result.add_error(ValidationError::schema_error(
1579 SchemaErrorCode::SchemaExtendsNotFound,
1580 format!("$extends reference not found: {}", ref_str),
1581 &ref_path,
1582 locator.get_location(&ref_path),
1583 ));
1584 }
1585 }
1587 }
1588
1589 fn validate_altnames(
1591 &self,
1592 altnames_val: &Value,
1593 locator: &JsonSourceLocator,
1594 result: &mut ValidationResult,
1595 path: &str,
1596 ) {
1597 let altnames_path = format!("{}/altnames", path);
1598
1599 match altnames_val {
1600 Value::Object(obj) => {
1601 for (key, value) in obj {
1602 if !value.is_string() {
1603 result.add_error(ValidationError::schema_error(
1604 SchemaErrorCode::SchemaAltnamesValueNotString,
1605 format!("altnames value for '{}' must be a string", key),
1606 &format!("{}/{}", altnames_path, key),
1607 locator.get_location(&format!("{}/{}", altnames_path, key)),
1608 ));
1609 }
1610 }
1611 }
1612 _ => {
1613 result.add_error(ValidationError::schema_error(
1614 SchemaErrorCode::SchemaAltnamesNotObject,
1615 "altnames must be an object",
1616 &altnames_path,
1617 locator.get_location(&altnames_path),
1618 ));
1619 }
1620 }
1621 }
1622
1623 fn check_extension_keywords(
1625 &self,
1626 obj: &serde_json::Map<String, Value>,
1627 locator: &JsonSourceLocator,
1628 result: &mut ValidationResult,
1629 path: &str,
1630 enabled_extensions: &HashSet<&str>,
1631 ) {
1632 let validation_enabled = enabled_extensions.contains("JSONStructureValidation");
1633 let composition_enabled = enabled_extensions.contains("JSONStructureConditionalComposition");
1634
1635 for (key, _) in obj {
1636 if VALIDATION_EXTENSION_KEYWORDS.contains(&key.as_str()) && !validation_enabled {
1638 result.add_error(ValidationError::schema_warning(
1639 SchemaErrorCode::SchemaExtensionKeywordWithoutUses,
1640 format!(
1641 "Validation extension keyword '{}' is used but validation extensions are not enabled. \
1642 Add '\"$uses\": [\"JSONStructureValidation\"]' to enable validation, or this keyword will be ignored.",
1643 key
1644 ),
1645 &format!("{}/{}", path, key),
1646 locator.get_location(&format!("{}/{}", path, key)),
1647 ));
1648 }
1649
1650 if COMPOSITION_KEYWORDS.contains(&key.as_str()) && !composition_enabled {
1652 result.add_error(ValidationError::schema_warning(
1653 SchemaErrorCode::SchemaExtensionKeywordWithoutUses,
1654 format!(
1655 "Conditional composition keyword '{}' is used but extensions are not enabled. \
1656 Add '\"$uses\": [\"JSONStructureConditionalComposition\"]' to enable.",
1657 key
1658 ),
1659 &format!("{}/{}", path, key),
1660 locator.get_location(&format!("{}/{}", path, key)),
1661 ));
1662 }
1663 }
1664 }
1665}
1666
1667#[cfg(test)]
1668mod tests {
1669 use super::*;
1670
1671 #[test]
1672 fn test_valid_simple_schema() {
1673 let schema = r#"{
1674 "$id": "https://example.com/schema",
1675 "name": "TestSchema",
1676 "type": "string"
1677 }"#;
1678
1679 let validator = SchemaValidator::new();
1680 let result = validator.validate(schema);
1681 assert!(result.is_valid());
1682 }
1683
1684 #[test]
1685 fn test_missing_id() {
1686 let schema = r#"{
1687 "name": "TestSchema",
1688 "type": "string"
1689 }"#;
1690
1691 let validator = SchemaValidator::new();
1692 let result = validator.validate(schema);
1693 assert!(!result.is_valid());
1694 }
1695
1696 #[test]
1697 fn test_missing_name_with_type() {
1698 let schema = r#"{
1699 "$id": "https://example.com/schema",
1700 "type": "string"
1701 }"#;
1702
1703 let validator = SchemaValidator::new();
1704 let result = validator.validate(schema);
1705 assert!(!result.is_valid());
1706 }
1707
1708 #[test]
1709 fn test_invalid_type() {
1710 let schema = r#"{
1711 "$id": "https://example.com/schema",
1712 "name": "TestSchema",
1713 "type": "invalid_type"
1714 }"#;
1715
1716 let validator = SchemaValidator::new();
1717 let result = validator.validate(schema);
1718 assert!(!result.is_valid());
1719 }
1720
1721 #[test]
1722 fn test_array_missing_items() {
1723 let schema = r#"{
1724 "$id": "https://example.com/schema",
1725 "name": "TestSchema",
1726 "type": "array"
1727 }"#;
1728
1729 let validator = SchemaValidator::new();
1730 let result = validator.validate(schema);
1731 assert!(!result.is_valid());
1732 }
1733
1734 #[test]
1735 fn test_map_missing_values() {
1736 let schema = r#"{
1737 "$id": "https://example.com/schema",
1738 "name": "TestSchema",
1739 "type": "map"
1740 }"#;
1741
1742 let validator = SchemaValidator::new();
1743 let result = validator.validate(schema);
1744 assert!(!result.is_valid());
1745 }
1746
1747 #[test]
1748 fn test_tuple_valid() {
1749 let schema = r#"{
1750 "$id": "https://example.com/schema",
1751 "name": "TestSchema",
1752 "type": "tuple",
1753 "properties": {
1754 "first": { "type": "string" },
1755 "second": { "type": "int32" }
1756 },
1757 "tuple": ["first", "second"]
1758 }"#;
1759
1760 let validator = SchemaValidator::new();
1761 let result = validator.validate(schema);
1762 assert!(result.is_valid());
1763 }
1764
1765 #[test]
1766 fn test_choice_valid() {
1767 let schema = r#"{
1768 "$id": "https://example.com/schema",
1769 "name": "TestSchema",
1770 "type": "choice",
1771 "selector": "kind",
1772 "choices": {
1773 "text": { "type": "string" },
1774 "number": { "type": "int32" }
1775 }
1776 }"#;
1777
1778 let validator = SchemaValidator::new();
1779 let result = validator.validate(schema);
1780 assert!(result.is_valid());
1781 }
1782
1783 #[test]
1784 fn test_enum_empty() {
1785 let schema = r#"{
1786 "$id": "https://example.com/schema",
1787 "name": "TestSchema",
1788 "type": "string",
1789 "enum": []
1790 }"#;
1791
1792 let validator = SchemaValidator::new();
1793 let result = validator.validate(schema);
1794 assert!(!result.is_valid());
1795 }
1796
1797 #[test]
1798 fn test_ref_to_definition() {
1799 let schema = r##"{
1800 "$id": "https://example.com/schema",
1801 "name": "TestSchema",
1802 "type": "object",
1803 "definitions": {
1804 "Inner": {
1805 "type": "string"
1806 }
1807 },
1808 "properties": {
1809 "value": { "type": { "$ref": "#/definitions/Inner" } }
1810 }
1811 }"##;
1812
1813 let validator = SchemaValidator::new();
1814 let result = validator.validate(schema);
1815 for err in result.all_errors() {
1816 println!("Error: {:?}", err);
1817 }
1818 assert!(result.is_valid(), "Schema with valid ref should pass");
1819 }
1820
1821 #[test]
1822 fn test_ref_undefined() {
1823 let schema = r##"{
1824 "$id": "https://example.com/schema",
1825 "name": "TestSchema",
1826 "type": "object",
1827 "properties": {
1828 "value": { "type": { "$ref": "#/definitions/Undefined" } }
1829 }
1830 }"##;
1831
1832 let validator = SchemaValidator::new();
1833 let result = validator.validate(schema);
1834 assert!(!result.is_valid(), "Schema with undefined ref should fail");
1835 }
1836
1837 #[test]
1838 fn test_union_type() {
1839 let schema = r##"{
1840 "$id": "https://example.com/schema",
1841 "name": "TestSchema",
1842 "type": "object",
1843 "properties": {
1844 "value": { "type": ["string", "null"] }
1845 }
1846 }"##;
1847
1848 let validator = SchemaValidator::new();
1849 let result = validator.validate(schema);
1850 for err in result.all_errors() {
1851 println!("Error: {:?}", err);
1852 }
1853 assert!(result.is_valid(), "Schema with union type should pass");
1854 }
1855}