1use crate::scalar::{ScalarType, ScalarValue};
7use crate::yaml::{Document, Mapping, Scalar, Sequence, TaggedNode};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum ValidationErrorKind {
12 TypeNotAllowed {
14 found_type: ScalarType,
16 allowed_types: Vec<ScalarType>,
18 },
19 CustomConstraintFailed {
21 constraint_name: String,
23 actual_value: String,
25 },
26 CoercionFailed {
28 from_type: ScalarType,
30 to_types: Vec<ScalarType>,
32 },
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ValidationError {
38 pub kind: ValidationErrorKind,
40 pub path: String,
42 pub schema_name: String,
44}
45
46impl ValidationError {
47 pub fn type_not_allowed(
49 path: impl Into<String>,
50 schema_name: impl Into<String>,
51 found_type: ScalarType,
52 allowed_types: Vec<ScalarType>,
53 ) -> Self {
54 Self {
55 kind: ValidationErrorKind::TypeNotAllowed {
56 found_type,
57 allowed_types,
58 },
59 path: path.into(),
60 schema_name: schema_name.into(),
61 }
62 }
63
64 pub fn custom_constraint_failed(
66 path: impl Into<String>,
67 schema_name: impl Into<String>,
68 constraint_name: impl Into<String>,
69 actual_value: impl Into<String>,
70 ) -> Self {
71 Self {
72 kind: ValidationErrorKind::CustomConstraintFailed {
73 constraint_name: constraint_name.into(),
74 actual_value: actual_value.into(),
75 },
76 path: path.into(),
77 schema_name: schema_name.into(),
78 }
79 }
80
81 pub fn coercion_failed(
83 path: impl Into<String>,
84 schema_name: impl Into<String>,
85 from_type: ScalarType,
86 to_types: Vec<ScalarType>,
87 ) -> Self {
88 Self {
89 kind: ValidationErrorKind::CoercionFailed {
90 from_type,
91 to_types,
92 },
93 path: path.into(),
94 schema_name: schema_name.into(),
95 }
96 }
97
98 pub fn message(&self) -> String {
100 match &self.kind {
101 ValidationErrorKind::TypeNotAllowed {
102 found_type,
103 allowed_types,
104 } => {
105 format!(
106 "type {:?} not allowed in {} schema, expected one of {:?}",
107 found_type, self.schema_name, allowed_types
108 )
109 }
110 ValidationErrorKind::CustomConstraintFailed {
111 constraint_name,
112 actual_value,
113 } => {
114 format!(
115 "custom constraint '{}' failed for value '{}'",
116 constraint_name, actual_value
117 )
118 }
119 ValidationErrorKind::CoercionFailed {
120 from_type,
121 to_types,
122 } => {
123 format!(
124 "cannot coerce {:?} to any of {:?} in {} schema",
125 from_type, to_types, self.schema_name
126 )
127 }
128 }
129 }
130}
131
132impl std::fmt::Display for ValidationError {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 write!(f, "Validation error at {}: {}", self.path, self.message())
135 }
136}
137
138impl std::error::Error for ValidationError {}
139
140pub type ValidationResult<T> = Result<T, Vec<ValidationError>>;
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum CustomValidationResult {
146 Valid,
148 Invalid {
150 constraint: String,
152 reason: String,
154 },
155}
156
157impl CustomValidationResult {
158 pub fn invalid(constraint: impl Into<String>, reason: impl Into<String>) -> Self {
160 Self::Invalid {
161 constraint: constraint.into(),
162 reason: reason.into(),
163 }
164 }
165
166 pub fn is_valid(&self) -> bool {
168 matches!(self, Self::Valid)
169 }
170
171 pub fn is_invalid(&self) -> bool {
173 matches!(self, Self::Invalid { .. })
174 }
175}
176
177pub type CustomValidator = Box<dyn Fn(&str, &str) -> CustomValidationResult + Send + Sync>;
181
182pub struct CustomSchema {
184 pub name: String,
186 pub allowed_types: Vec<ScalarType>,
188 pub custom_validators: std::collections::HashMap<ScalarType, CustomValidator>,
190 pub allow_coercion: bool,
192}
193
194impl std::fmt::Debug for CustomSchema {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.debug_struct("CustomSchema")
197 .field("name", &self.name)
198 .field("allowed_types", &self.allowed_types)
199 .field("allow_coercion", &self.allow_coercion)
200 .field(
201 "validators",
202 &format!("<{} validators>", self.custom_validators.len()),
203 )
204 .finish()
205 }
206}
207
208impl CustomSchema {
209 pub fn new(name: impl Into<String>) -> Self {
211 Self {
212 name: name.into(),
213 allowed_types: Vec::new(),
214 custom_validators: std::collections::HashMap::new(),
215 allow_coercion: true,
216 }
217 }
218
219 pub fn allow_type(mut self, scalar_type: ScalarType) -> Self {
221 if !self.allowed_types.contains(&scalar_type) {
222 self.allowed_types.push(scalar_type);
223 }
224 self
225 }
226
227 pub fn allow_types(mut self, types: &[ScalarType]) -> Self {
229 for &scalar_type in types {
230 if !self.allowed_types.contains(&scalar_type) {
231 self.allowed_types.push(scalar_type);
232 }
233 }
234 self
235 }
236
237 pub fn with_validator<F>(mut self, scalar_type: ScalarType, validator: F) -> Self
239 where
240 F: Fn(&str, &str) -> CustomValidationResult + Send + Sync + 'static,
241 {
242 self.custom_validators
243 .insert(scalar_type, Box::new(validator));
244 self
245 }
246
247 pub fn strict(mut self) -> Self {
249 self.allow_coercion = false;
250 self
251 }
252
253 pub fn allows_type(&self, scalar_type: ScalarType) -> bool {
255 self.allowed_types.contains(&scalar_type)
256 }
257
258 pub fn validate_scalar(&self, content: &str, path: &str) -> Result<(), ValidationError> {
260 let scalar_value = ScalarValue::parse(content.trim());
261 let scalar_type = scalar_value.scalar_type();
262
263 if !self.allows_type(scalar_type) {
265 if self.allow_coercion {
266 let mut coerced = false;
268 for &allowed_type in &self.allowed_types {
269 if scalar_value.coerce_to_type(allowed_type).is_some() {
270 coerced = true;
271 break;
272 }
273 }
274 if !coerced {
275 return Err(ValidationError::coercion_failed(
276 path,
277 &self.name,
278 scalar_type,
279 self.allowed_types.clone(),
280 ));
281 }
282 } else {
283 return Err(ValidationError::type_not_allowed(
284 path,
285 &self.name,
286 scalar_type,
287 self.allowed_types.clone(),
288 ));
289 }
290 }
291
292 if let Some(validator) = self.custom_validators.get(&scalar_type) {
294 let result = validator(content.trim(), path);
295 if let CustomValidationResult::Invalid { constraint, reason } = result {
296 return Err(ValidationError::custom_constraint_failed(
297 path,
298 &self.name,
299 format!("{}: {}", constraint, reason),
300 content.trim(),
301 ));
302 }
303 }
304
305 Ok(())
306 }
307}
308
309#[derive(Debug)]
311pub enum Schema {
312 Failsafe,
314 Json,
316 Core,
318 Custom(CustomSchema),
320}
321
322impl PartialEq for Schema {
323 fn eq(&self, other: &Self) -> bool {
324 match (self, other) {
325 (Schema::Failsafe, Schema::Failsafe) => true,
326 (Schema::Json, Schema::Json) => true,
327 (Schema::Core, Schema::Core) => true,
328 (Schema::Custom(a), Schema::Custom(b)) => a.name == b.name,
329 _ => false,
330 }
331 }
332}
333
334impl Schema {
335 pub fn name(&self) -> &str {
337 match self {
338 Schema::Failsafe => "failsafe",
339 Schema::Json => "json",
340 Schema::Core => "core",
341 Schema::Custom(custom) => &custom.name,
342 }
343 }
344
345 pub fn allows_scalar_type(&self, scalar_type: ScalarType) -> bool {
347 match self {
348 Schema::Failsafe => matches!(scalar_type, ScalarType::String),
349 Schema::Json => matches!(
350 scalar_type,
351 ScalarType::String
352 | ScalarType::Integer
353 | ScalarType::Float
354 | ScalarType::Boolean
355 | ScalarType::Null
356 ),
357 Schema::Core => true, Schema::Custom(custom) => custom.allows_type(scalar_type),
359 }
360 }
361
362 pub fn allowed_scalar_types(&self) -> Vec<ScalarType> {
364 match self {
365 Schema::Failsafe => vec![ScalarType::String],
366 Schema::Json => vec![
367 ScalarType::String,
368 ScalarType::Integer,
369 ScalarType::Float,
370 ScalarType::Boolean,
371 ScalarType::Null,
372 ],
373 Schema::Core => vec![
374 ScalarType::String,
375 ScalarType::Integer,
376 ScalarType::Float,
377 ScalarType::Boolean,
378 ScalarType::Null,
379 #[cfg(feature = "base64")]
380 ScalarType::Binary,
381 ScalarType::Timestamp,
382 ScalarType::Regex,
383 ],
384 Schema::Custom(custom) => custom.allowed_types.clone(),
385 }
386 }
387}
388
389#[derive(Debug)]
391pub struct SchemaValidator {
392 schema: Schema,
393 strict: bool,
394}
395
396impl SchemaValidator {
397 pub fn new(schema: Schema) -> Self {
399 Self {
400 schema,
401 strict: false,
402 }
403 }
404
405 pub fn failsafe() -> Self {
407 Self::new(Schema::Failsafe)
408 }
409
410 pub fn json() -> Self {
412 Self::new(Schema::Json)
413 }
414
415 pub fn core() -> Self {
417 Self::new(Schema::Core)
418 }
419
420 pub fn custom(schema: CustomSchema) -> Self {
422 Self::new(Schema::Custom(schema))
423 }
424
425 pub fn strict(mut self) -> Self {
427 self.strict = true;
428 self
429 }
430
431 pub fn schema(&self) -> &Schema {
433 &self.schema
434 }
435
436 pub fn validate(&self, document: &Document) -> ValidationResult<()> {
438 let mut errors = Vec::new();
439 self.validate_document(document, "root", &mut errors);
440
441 if errors.is_empty() {
442 Ok(())
443 } else {
444 Err(errors)
445 }
446 }
447
448 fn validate_document(
450 &self,
451 document: &Document,
452 path: &str,
453 errors: &mut Vec<ValidationError>,
454 ) {
455 if let Some(scalar) = document.as_scalar() {
456 self.validate_scalar(&scalar, path, errors);
457 } else if let Some(sequence) = document.as_sequence() {
458 self.validate_sequence(&sequence, path, errors);
459 } else if let Some(mapping) = document.as_mapping() {
460 self.validate_mapping(&mapping, path, errors);
461 }
462 }
464
465 fn validate_scalar(&self, scalar: &Scalar, path: &str, errors: &mut Vec<ValidationError>) {
467 let content = scalar.as_string();
468
469 if let Schema::Custom(custom_schema) = &self.schema {
471 if let Err(error) = custom_schema.validate_scalar(&content, path) {
472 errors.push(error);
473 }
474 return;
475 }
476
477 let scalar_value = ScalarValue::parse(content.trim());
479 let scalar_type = scalar_value.scalar_type();
480
481 if !self.schema.allows_scalar_type(scalar_type) {
482 if !self.strict {
484 let allowed_types = self.schema.allowed_scalar_types();
485 let mut coercion_successful = false;
486
487 for allowed_type in allowed_types {
488 if scalar_value.coerce_to_type(allowed_type).is_some() {
489 coercion_successful = true;
490 break;
491 }
492 }
493
494 if !coercion_successful {
495 errors.push(ValidationError::coercion_failed(
496 path,
497 self.schema.name(),
498 scalar_type,
499 self.schema.allowed_scalar_types(),
500 ));
501 }
502 } else {
503 errors.push(ValidationError::type_not_allowed(
504 path,
505 self.schema.name(),
506 scalar_type,
507 self.schema.allowed_scalar_types(),
508 ));
509 }
510 }
511 }
512
513 fn validate_sequence(&self, seq: &Sequence, path: &str, errors: &mut Vec<ValidationError>) {
515 for (i, item) in seq.items().enumerate() {
516 let item_path = format!("{}[{}]", path, i);
517 self.validate_node(&item, &item_path, errors);
518 }
519 }
520
521 fn validate_mapping(&self, map: &Mapping, path: &str, errors: &mut Vec<ValidationError>) {
523 for (key_node, value_node) in map.pairs() {
524 let key_name = key_node.text().to_string().trim().to_string();
526
527 let value_path = format!("{}.{}", path, key_name);
530 self.validate_node(&value_node, &value_path, errors);
531 }
532 }
533
534 fn validate_node(
536 &self,
537 node: &rowan::SyntaxNode<crate::yaml::Lang>,
538 path: &str,
539 errors: &mut Vec<ValidationError>,
540 ) {
541 use crate::yaml::{extract_mapping, extract_scalar, extract_sequence, extract_tagged_node};
542
543 if let Some(scalar) = extract_scalar(node) {
545 self.validate_scalar(&scalar, path, errors);
546 } else if let Some(tagged_node) = extract_tagged_node(node) {
547 self.validate_tagged_node(&tagged_node, path, errors);
548 } else if let Some(sequence) = extract_sequence(node) {
549 self.validate_sequence(&sequence, path, errors);
550 } else if let Some(mapping) = extract_mapping(node) {
551 self.validate_mapping(&mapping, path, errors);
552 }
553 }
555
556 fn validate_tagged_node(
558 &self,
559 tagged_node: &TaggedNode,
560 path: &str,
561 errors: &mut Vec<ValidationError>,
562 ) {
563 if let Schema::Custom(custom_schema) = &self.schema {
565 let content = tagged_node.to_string();
566 if let Err(error) = custom_schema.validate_scalar(&content, path) {
567 errors.push(error);
568 }
569 return;
570 }
571
572 let scalar_type = self.get_tagged_node_type(tagged_node);
574
575 if !self.schema.allows_scalar_type(scalar_type) {
576 errors.push(ValidationError::type_not_allowed(
578 path,
579 self.schema.name(),
580 scalar_type,
581 self.schema.allowed_scalar_types(),
582 ));
583 }
584 }
585
586 fn get_tagged_node_type(&self, tagged_node: &TaggedNode) -> ScalarType {
588 match tagged_node.tag().as_deref() {
589 Some("!!timestamp") => ScalarType::Timestamp,
590 Some("!!regex") => ScalarType::Regex,
591 Some("!!binary") => {
592 #[cfg(feature = "base64")]
593 return ScalarType::Binary;
594 #[cfg(not(feature = "base64"))]
595 return ScalarType::String;
596 }
597 _ => ScalarType::String,
598 }
599 }
600
601 pub fn can_coerce(&self, document: &Document) -> ValidationResult<()> {
603 if self.strict {
604 return self.validate(document);
605 }
606
607 let mut errors = Vec::new();
608 self.check_coercion(document, "root", &mut errors);
609
610 if errors.is_empty() {
611 Ok(())
612 } else {
613 Err(errors)
614 }
615 }
616
617 fn check_coercion(&self, document: &Document, path: &str, errors: &mut Vec<ValidationError>) {
619 if let Some(scalar) = document.as_scalar() {
620 let scalar_value = ScalarValue::parse(scalar.as_string().trim());
621 let scalar_type = scalar_value.scalar_type();
622
623 if !self.schema.allows_scalar_type(scalar_type) {
624 let allowed_types = self.schema.allowed_scalar_types();
626 let mut coerced = false;
627
628 for allowed_type in allowed_types {
629 if scalar_value.coerce_to_type(allowed_type).is_some() {
630 coerced = true;
631 break;
632 }
633 }
634
635 if !coerced {
636 errors.push(ValidationError::coercion_failed(
637 path,
638 self.schema.name(),
639 scalar_type,
640 self.schema.allowed_scalar_types(),
641 ));
642 }
643 }
644 } else if let Some(sequence) = document.as_sequence() {
645 for (i, item) in sequence.items().enumerate() {
647 let item_path = format!("{}[{}]", path, i);
648 self.check_coercion_node(&item, &item_path, errors);
649 }
650 } else if let Some(mapping) = document.as_mapping() {
651 for (key_node, value_node) in mapping.pairs() {
653 let key_name = key_node.text().to_string().trim().to_string();
654 let value_path = format!("{}.{}", path, key_name);
655 self.check_coercion_node(&value_node, &value_path, errors);
656 }
657 }
658 }
659
660 fn check_coercion_node(
662 &self,
663 node: &rowan::SyntaxNode<crate::yaml::Lang>,
664 path: &str,
665 errors: &mut Vec<ValidationError>,
666 ) {
667 use crate::yaml::{extract_mapping, extract_scalar, extract_sequence, extract_tagged_node};
668
669 if let Some(scalar) = extract_scalar(node) {
671 let scalar_value = ScalarValue::parse(scalar.as_string().trim());
672 let scalar_type = scalar_value.scalar_type();
673
674 if !self.schema.allows_scalar_type(scalar_type) {
675 let allowed_types = self.schema.allowed_scalar_types();
676 let mut coerced = false;
677
678 for allowed_type in allowed_types {
679 if scalar_value.coerce_to_type(allowed_type).is_some() {
680 coerced = true;
681 break;
682 }
683 }
684
685 if !coerced {
686 errors.push(ValidationError::coercion_failed(
687 path,
688 self.schema.name(),
689 scalar_type,
690 self.schema.allowed_scalar_types(),
691 ));
692 }
693 }
694 } else if let Some(tagged_node) = extract_tagged_node(node) {
695 let scalar_type = self.get_tagged_node_type(&tagged_node);
696
697 if !self.schema.allows_scalar_type(scalar_type) {
698 errors.push(ValidationError::type_not_allowed(
700 path,
701 self.schema.name(),
702 scalar_type,
703 self.schema.allowed_scalar_types(),
704 ));
705 }
706 } else if let Some(sequence) = extract_sequence(node) {
707 for (i, item) in sequence.items().enumerate() {
708 let item_path = format!("{}[{}]", path, i);
709 self.check_coercion_node(&item, &item_path, errors);
710 }
711 } else if let Some(mapping) = extract_mapping(node) {
712 for (key_node, value_node) in mapping.pairs() {
713 let key_name = key_node.text().to_string().trim().to_string();
714 let value_path = format!("{}.{}", path, key_name);
715 self.check_coercion_node(&value_node, &value_path, errors);
716 }
717 }
718 }
719}
720
721#[cfg(test)]
722mod tests {
723 use super::*;
724 use crate::yaml::Document;
725 use rowan::ast::AstNode;
726
727 #[test]
728 fn test_schema_names() {
729 assert_eq!(Schema::Failsafe.name(), "failsafe");
730 assert_eq!(Schema::Json.name(), "json");
731 assert_eq!(Schema::Core.name(), "core");
732 }
733
734 #[test]
735 fn test_failsafe_schema_allows_only_strings() {
736 let schema = Schema::Failsafe;
737
738 assert!(schema.allows_scalar_type(ScalarType::String));
739 assert!(!schema.allows_scalar_type(ScalarType::Integer));
740 assert!(!schema.allows_scalar_type(ScalarType::Float));
741 assert!(!schema.allows_scalar_type(ScalarType::Boolean));
742 assert!(!schema.allows_scalar_type(ScalarType::Null));
743 #[cfg(feature = "base64")]
744 assert!(!schema.allows_scalar_type(ScalarType::Binary));
745 assert!(!schema.allows_scalar_type(ScalarType::Timestamp));
746 assert!(!schema.allows_scalar_type(ScalarType::Regex));
747 }
748
749 #[test]
750 fn test_json_schema_allows_json_types() {
751 let schema = Schema::Json;
752
753 assert!(schema.allows_scalar_type(ScalarType::String));
754 assert!(schema.allows_scalar_type(ScalarType::Integer));
755 assert!(schema.allows_scalar_type(ScalarType::Float));
756 assert!(schema.allows_scalar_type(ScalarType::Boolean));
757 assert!(schema.allows_scalar_type(ScalarType::Null));
758 #[cfg(feature = "base64")]
759 assert!(!schema.allows_scalar_type(ScalarType::Binary));
760 assert!(!schema.allows_scalar_type(ScalarType::Timestamp));
761 assert!(!schema.allows_scalar_type(ScalarType::Regex));
762 }
763
764 #[test]
765 fn test_core_schema_allows_all_types() {
766 let schema = Schema::Core;
767
768 assert!(schema.allows_scalar_type(ScalarType::String));
769 assert!(schema.allows_scalar_type(ScalarType::Integer));
770 assert!(schema.allows_scalar_type(ScalarType::Float));
771 assert!(schema.allows_scalar_type(ScalarType::Boolean));
772 assert!(schema.allows_scalar_type(ScalarType::Null));
773 #[cfg(feature = "base64")]
774 assert!(schema.allows_scalar_type(ScalarType::Binary));
775 assert!(schema.allows_scalar_type(ScalarType::Timestamp));
776 assert!(schema.allows_scalar_type(ScalarType::Regex));
777 }
778
779 #[test]
780 fn test_validator_creation() {
781 let failsafe = SchemaValidator::failsafe();
782 assert_eq!(*failsafe.schema(), Schema::Failsafe);
783 assert!(!failsafe.strict);
784
785 let json = SchemaValidator::json();
786 assert_eq!(*json.schema(), Schema::Json);
787
788 let core = SchemaValidator::core();
789 assert_eq!(*core.schema(), Schema::Core);
790
791 let strict_validator = SchemaValidator::json().strict();
792 assert!(strict_validator.strict);
793 }
794
795 #[test]
796 fn test_validation_error_display() {
797 let error = ValidationError::type_not_allowed(
798 "root.items[0]",
799 "test-schema",
800 ScalarType::Integer,
801 vec![ScalarType::String],
802 );
803
804 assert_eq!(
805 format!("{}", error),
806 "Validation error at root.items[0]: type Integer not allowed in test-schema schema, expected one of [String]"
807 );
808 }
809
810 fn create_test_document(content: &str) -> Document {
811 use crate::yaml::YamlFile;
812 let parsed = content
813 .parse::<YamlFile>()
814 .expect("Failed to parse test YAML");
815 parsed.document().expect("Expected a document")
816 }
817
818 #[test]
819 fn test_failsafe_validation_success() {
820 let yaml_str = r#"
821name: "John Doe"
822items:
823 - "item1"
824 - "item2"
825nested:
826 key: "value"
827"#;
828 let document = create_test_document(yaml_str);
829 let validator = SchemaValidator::failsafe();
830
831 assert!(validator.validate(&document).is_ok());
832 }
833
834 #[test]
835 fn test_json_validation_success() {
836 let yaml_str = r#"
837name: "John Doe"
838age: 30
839height: 5.9
840active: true
841metadata: null
842items:
843 - "item1"
844 - 42
845 - true
846"#;
847 let document = create_test_document(yaml_str);
848 let validator = SchemaValidator::json();
849
850 assert!(validator.validate(&document).is_ok());
851 }
852
853 #[test]
854 fn test_core_validation_success() {
855 let yaml_str = r#"
856name: "John Doe"
857age: 30
858birth_date: !!timestamp "2001-12-15T02:59:43.1Z"
859pattern: !!regex '\d{3}-\d{4}'
860"#;
861 let document = create_test_document(yaml_str);
862 let validator = SchemaValidator::core();
863
864 assert!(validator.validate(&document).is_ok());
865 }
866
867 #[test]
868 fn test_failsafe_validation_failure() {
869 let yaml_str = r#"
870name: "John"
871age: 30
872active: true
873"#;
874 let document = create_test_document(yaml_str);
875 let validator = SchemaValidator::failsafe().strict();
876
877 let result = validator.validate(&document);
878 assert!(result.is_err());
880 let errors = result.unwrap_err();
881 assert!(!errors.is_empty());
882
883 assert!(errors.iter().all(|e| e.schema_name == "failsafe"));
885 assert!(errors
886 .iter()
887 .all(|e| matches!(&e.kind, ValidationErrorKind::TypeNotAllowed { .. })));
888 }
889
890 #[test]
891 fn test_json_validation_with_yaml_specific_types() {
892 let yaml_str = r#"
893timestamp: !!timestamp "2023-12-25T10:30:45Z"
894pattern: !!regex '\d+'
895"#;
896 let document = create_test_document(yaml_str);
897 let validator = SchemaValidator::json().strict();
898
899 let result = validator.validate(&document);
900
901 if result.is_ok() {
903 println!("JSON validation unexpectedly passed!");
904
905 println!("Document is mapping: {}", document.as_mapping().is_some());
906 println!("Document is sequence: {}", document.as_sequence().is_some());
907 println!("Document is scalar: {}", document.as_scalar().is_some());
908
909 if let Some(mapping) = document.as_mapping() {
910 println!("Mapping has {} pairs", mapping.pairs().count());
911 for (key, value) in mapping.pairs() {
912 if let Some(scalar) = Scalar::cast(value.clone()) {
913 let scalar_value = ScalarValue::parse(scalar.as_string().trim());
914 println!(
915 "JSON test - Key '{}' -> Value: '{}' -> Type: {:?}",
916 key.text().to_string().trim(),
917 scalar.as_string().trim(),
918 scalar_value.scalar_type()
919 );
920 } else {
921 println!(
922 "Value for key '{}' is not a scalar. Node kind: {:?}",
923 key.text().to_string().trim(),
924 value.kind()
925 );
926 }
927 }
928 } else {
929 println!("Document is not a mapping");
930 }
931 } else {
932 println!("JSON validation correctly failed");
933 }
934
935 assert!(result.is_err());
937 let errors = result.unwrap_err();
938 assert!(!errors.is_empty());
939
940 assert!(errors.iter().all(|e| e.schema_name == "json"));
942 assert!(errors
943 .iter()
944 .all(|e| matches!(&e.kind, ValidationErrorKind::TypeNotAllowed { .. })));
945 }
946
947 #[test]
948 fn test_strict_mode_validation() {
949 let yaml_str = r#"
951count: 42
952active: true
953"#;
954 let document = create_test_document(yaml_str);
955
956 let validator = SchemaValidator::failsafe();
958 assert!(validator.can_coerce(&document).is_ok());
959
960 let strict_validator = SchemaValidator::failsafe().strict();
962 let result = strict_validator.validate(&document);
963 assert!(result.is_err());
964
965 let string_yaml = r#"
967name: hello
968message: world
969"#;
970 let string_document = create_test_document(string_yaml);
971
972 let non_strict_result = validator.validate(&string_document);
974 let strict_result = strict_validator.validate(&string_document);
975
976 if strict_result.is_err() {
978 println!("String validation failed!");
979 if let Some(mapping) = string_document.as_mapping() {
980 for (key, value) in mapping.pairs() {
981 if let Some(scalar) = Scalar::cast(value.clone()) {
982 let scalar_value = ScalarValue::parse(scalar.as_string().trim());
983 println!(
984 "String - Key '{}' -> Value: '{}' -> Type: {:?}",
985 key.text().to_string().trim(),
986 scalar.as_string().trim(),
987 scalar_value.scalar_type()
988 );
989 }
990 }
991 }
992 if let Err(ref errors) = strict_result {
993 for error in errors {
994 println!(" - {}: {}", error.path, error.message());
995 }
996 }
997 }
998
999 assert!(non_strict_result.is_ok());
1004 assert!(strict_result.is_ok());
1005 }
1006
1007 #[test]
1008 fn test_validation_error_paths() {
1009 let yaml_str = r#"
1010users:
1011 - name: "John"
1012 age: 30
1013 - name: "Jane"
1014 active: true
1015"#;
1016 let document = create_test_document(yaml_str);
1017 let validator = SchemaValidator::failsafe();
1018
1019 let result = validator.validate(&document);
1020 if let Err(errors) = result {
1021 for error in &errors {
1023 assert!(!error.path.is_empty());
1024 assert!(
1025 error.path.starts_with("root"),
1026 "expected path to start with 'root', got: {:?}",
1027 error.path
1028 );
1029 }
1030 }
1031 }
1032
1033 #[test]
1034 fn test_schema_type_lists() {
1035 assert_eq!(
1036 Schema::Failsafe.allowed_scalar_types(),
1037 vec![ScalarType::String]
1038 );
1039
1040 assert_eq!(
1041 Schema::Json.allowed_scalar_types(),
1042 vec![
1043 ScalarType::String,
1044 ScalarType::Integer,
1045 ScalarType::Float,
1046 ScalarType::Boolean,
1047 ScalarType::Null,
1048 ]
1049 );
1050
1051 let core_types = Schema::Core.allowed_scalar_types();
1052 let mut expected_core = vec![
1055 ScalarType::String,
1056 ScalarType::Integer,
1057 ScalarType::Float,
1058 ScalarType::Boolean,
1059 ScalarType::Null,
1060 ScalarType::Timestamp,
1061 ScalarType::Regex,
1062 ];
1063 #[cfg(feature = "base64")]
1064 expected_core.insert(5, ScalarType::Binary);
1065 assert_eq!(core_types, expected_core);
1066 }
1067
1068 #[test]
1069 fn test_deep_sequence_validation() {
1070 let yaml_str = r#"
1071numbers:
1072 - 1
1073 - 2.5
1074 - "three"
1075 - true
1076"#;
1077 let document = create_test_document(yaml_str);
1078
1079 let failsafe_validator = SchemaValidator::failsafe().strict();
1081 let result = failsafe_validator.validate(&document);
1082 assert!(result.is_err());
1083 let errors = result.unwrap_err();
1084 assert!(!errors.is_empty());
1085
1086 let json_validator = SchemaValidator::json();
1088 let result = json_validator.validate(&document);
1089 assert!(result.is_ok());
1090
1091 let core_validator = SchemaValidator::core();
1093 let result = core_validator.validate(&document);
1094 assert!(result.is_ok());
1095 }
1096
1097 #[test]
1098 fn test_deep_nested_mapping_validation() {
1099 let yaml_str = r#"
1100user:
1101 name: "John"
1102 details:
1103 age: 30
1104 active: true
1105 scores:
1106 - 95
1107 - 87.5
1108"#;
1109 let document = create_test_document(yaml_str);
1110
1111 let failsafe_validator = SchemaValidator::failsafe().strict();
1113 let result = failsafe_validator.validate(&document);
1114 assert!(result.is_err());
1115 let errors = result.unwrap_err();
1116 assert!(!errors.is_empty());
1117 let paths: Vec<&str> = errors.iter().map(|e| e.path.as_str()).collect();
1119 assert!(paths.contains(&"root.user.details.age"));
1120 assert!(paths.contains(&"root.user.details.scores[0]"));
1121
1122 let json_validator = SchemaValidator::json();
1124 let result = json_validator.validate(&document);
1125 assert!(result.is_ok());
1126 }
1127
1128 #[test]
1129 fn test_complex_yaml_types_validation() {
1130 let yaml_str = r#"
1131metadata:
1132 created: !!timestamp "2023-12-25T10:30:45Z"
1133 pattern: !!regex '\d{3}-\d{4}'
1134values:
1135 - !!timestamp "2023-01-01"
1136 - !!regex '[a-zA-Z]+'
1137"#;
1138 let document = create_test_document(yaml_str);
1139
1140 let failsafe_validator = SchemaValidator::failsafe().strict();
1142 let result = failsafe_validator.validate(&document);
1143 assert!(result.is_err());
1144
1145 let json_validator = SchemaValidator::json().strict();
1147 let result = json_validator.validate(&document);
1148 assert!(result.is_err());
1149
1150 let core_validator = SchemaValidator::core();
1152 let result = core_validator.validate(&document);
1153 assert!(result.is_ok());
1154 }
1155
1156 #[test]
1157 fn test_coercion_deep_validation() {
1158 let yaml_str = r#"
1159config:
1160 timeout: "30" # string that looks like number
1161 enabled: "true" # string that looks like boolean
1162 items:
1163 - "42"
1164 - "false"
1165"#;
1166 let document = create_test_document(yaml_str);
1167
1168 let json_validator = SchemaValidator::json();
1170 let result = json_validator.can_coerce(&document);
1171 assert!(result.is_ok());
1172
1173 let strict_json_validator = SchemaValidator::json().strict();
1175 let result = strict_json_validator.validate(&document);
1176 assert!(result.is_ok());
1178
1179 let problematic_yaml = r#"
1181data:
1182 timestamp: !!timestamp "2023-12-25"
1183"#;
1184 let problematic_doc = create_test_document(problematic_yaml);
1185 let result = strict_json_validator.validate(&problematic_doc);
1186 assert!(result.is_err());
1187 }
1188
1189 #[test]
1190 fn test_validation_error_paths_nested() {
1191 let yaml_str = r#"
1192users:
1193 - name: "Alice"
1194 metadata:
1195 created: !!timestamp "2023-01-01"
1196 tags:
1197 - "admin"
1198 - 42 # This should fail in failsafe
1199 - name: "Bob"
1200 active: true # This should fail in failsafe
1201"#;
1202 let document = create_test_document(yaml_str);
1203 let validator = SchemaValidator::failsafe().strict();
1204
1205 let result = validator.validate(&document);
1206 assert!(result.is_err());
1207 let errors = result.unwrap_err();
1208 assert!(!errors.is_empty());
1209
1210 let paths: Vec<&str> = errors.iter().map(|e| e.path.as_str()).collect();
1212
1213 assert!(paths.contains(&"root.users[0].metadata.created"));
1215 assert!(paths.contains(&"root.users[0].metadata.tags[1]"));
1216 assert!(paths.contains(&"root.users[1].active"));
1217
1218 for error in &errors {
1220 println!("Error at {}: {}", error.path, error.message());
1221 }
1222 }
1223
1224 #[test]
1225 fn test_yaml_1_2_spec_compliance() {
1226 let failsafe = Schema::Failsafe;
1230 assert!(failsafe.allows_scalar_type(ScalarType::String));
1231 assert!(!failsafe.allows_scalar_type(ScalarType::Integer));
1232 assert!(!failsafe.allows_scalar_type(ScalarType::Float));
1233 assert!(!failsafe.allows_scalar_type(ScalarType::Boolean));
1234 assert!(!failsafe.allows_scalar_type(ScalarType::Null));
1235 assert!(!failsafe.allows_scalar_type(ScalarType::Timestamp));
1236
1237 let json = Schema::Json;
1239 assert!(json.allows_scalar_type(ScalarType::String));
1240 assert!(json.allows_scalar_type(ScalarType::Integer));
1241 assert!(json.allows_scalar_type(ScalarType::Float));
1242 assert!(json.allows_scalar_type(ScalarType::Boolean));
1243 assert!(json.allows_scalar_type(ScalarType::Null));
1244 assert!(!json.allows_scalar_type(ScalarType::Timestamp)); assert!(!json.allows_scalar_type(ScalarType::Regex)); #[cfg(feature = "base64")]
1247 assert!(!json.allows_scalar_type(ScalarType::Binary)); let core = Schema::Core;
1251 assert!(core.allows_scalar_type(ScalarType::String));
1252 assert!(core.allows_scalar_type(ScalarType::Integer));
1253 assert!(core.allows_scalar_type(ScalarType::Float));
1254 assert!(core.allows_scalar_type(ScalarType::Boolean));
1255 assert!(core.allows_scalar_type(ScalarType::Null));
1256 assert!(core.allows_scalar_type(ScalarType::Timestamp));
1257 assert!(core.allows_scalar_type(ScalarType::Regex));
1258 #[cfg(feature = "base64")]
1259 assert!(core.allows_scalar_type(ScalarType::Binary));
1260
1261 assert_eq!(failsafe.name(), "failsafe");
1263 assert_eq!(json.name(), "json");
1264 assert_eq!(core.name(), "core");
1265 }
1266
1267 #[test]
1268 fn test_spec_compliant_validation_examples() {
1269 let failsafe_yaml = r#"
1273string: hello
1274number_as_string: "123"
1275"#;
1276 let failsafe_doc = create_test_document(failsafe_yaml);
1277 let failsafe_validator = SchemaValidator::failsafe();
1278 assert!(failsafe_validator.validate(&failsafe_doc).is_ok());
1280
1281 let json_yaml = r#"
1283string: "hello"
1284number: 42
1285float: 3.14
1286boolean: true
1287null_value: null
1288"#;
1289 let json_doc = create_test_document(json_yaml);
1290 let json_validator = SchemaValidator::json();
1291 assert!(json_validator.validate(&json_doc).is_ok());
1292
1293 let core_yaml = r#"
1295timestamp: 2023-01-01T00:00:00Z
1296regex: !!regex '[0-9]+'
1297binary: !!binary "SGVsbG8gV29ybGQ="
1298"#;
1299 let core_doc = create_test_document(core_yaml);
1300 let core_validator = SchemaValidator::core();
1301 assert!(core_validator.validate(&core_doc).is_ok());
1302
1303 let json_strict = SchemaValidator::json().strict();
1305 assert!(json_strict.validate(&core_doc).is_err());
1306 }
1307
1308 #[test]
1309 fn test_custom_schema_basic() {
1310 let custom_schema = CustomSchema::new("test")
1312 .allow_types(&[ScalarType::String, ScalarType::Integer])
1313 .strict(); let validator = SchemaValidator::custom(custom_schema);
1316
1317 let valid_yaml = r#"
1319name: hello world
1320count: 42
1321"#;
1322 let valid_doc = create_test_document(valid_yaml);
1323 let result = validator.validate(&valid_doc);
1324 if let Err(ref errors) = result {
1325 for error in errors {
1326 println!("Valid test error: {}", error);
1327 }
1328 }
1329 assert!(result.is_ok());
1330
1331 let invalid_yaml = r#"
1333name: hello world
1334enabled: true # boolean not allowed
1335"#;
1336 let invalid_doc = create_test_document(invalid_yaml);
1337 let result = validator.validate(&invalid_doc);
1338 assert!(result.is_err());
1339
1340 let errors = result.unwrap_err();
1341 assert_eq!(errors.len(), 1);
1342 assert_eq!(
1343 errors[0].message(),
1344 "type Boolean not allowed in test schema, expected one of [String, Integer]"
1345 );
1346 }
1347
1348 #[test]
1349 fn test_custom_schema_with_validators() {
1350 let custom_schema = CustomSchema::new("email-validation")
1352 .allow_type(ScalarType::String)
1353 .with_validator(ScalarType::String, |value, _path| {
1354 if value.contains('@') && value.contains('.') {
1355 CustomValidationResult::Valid
1356 } else {
1357 CustomValidationResult::invalid("email_format", "invalid email format")
1358 }
1359 });
1360
1361 let validator = SchemaValidator::custom(custom_schema);
1362
1363 let valid_yaml = r#"
1365email: "user@example.com"
1366"#;
1367 let valid_doc = create_test_document(valid_yaml);
1368 assert!(validator.validate(&valid_doc).is_ok());
1369
1370 let invalid_yaml = r#"
1372email: "not-an-email"
1373"#;
1374 let invalid_doc = create_test_document(invalid_yaml);
1375 let result = validator.validate(&invalid_doc);
1376 assert!(result.is_err());
1377
1378 let errors = result.unwrap_err();
1379 assert_eq!(errors.len(), 1);
1380 assert_eq!(
1381 errors[0].message(),
1382 "custom constraint 'email_format: invalid email format' failed for value 'not-an-email'"
1383 );
1384 }
1385
1386 #[test]
1387 fn test_custom_schema_integer_range() {
1388 let custom_schema = CustomSchema::new("port-validation")
1390 .allow_type(ScalarType::Integer)
1391 .with_validator(ScalarType::Integer, |value, _path| {
1392 if let Ok(port) = value.parse::<u16>() {
1393 if (1024..=65535).contains(&port) {
1394 CustomValidationResult::Valid
1395 } else {
1396 CustomValidationResult::invalid(
1397 "port_range",
1398 format!("port {} must be between 1024 and 65535", port),
1399 )
1400 }
1401 } else {
1402 CustomValidationResult::invalid(
1403 "integer_format",
1404 format!("invalid integer: {}", value),
1405 )
1406 }
1407 });
1408
1409 let validator = SchemaValidator::custom(custom_schema);
1410
1411 let valid_yaml = r#"
1413port: 8080
1414"#;
1415 let valid_doc = create_test_document(valid_yaml);
1416 assert!(validator.validate(&valid_doc).is_ok());
1417
1418 let invalid_yaml = r#"
1420port: 80
1421"#;
1422 let invalid_doc = create_test_document(invalid_yaml);
1423 let result = validator.validate(&invalid_doc);
1424 assert!(result.is_err());
1425
1426 let errors = result.unwrap_err();
1427 assert!(!errors.is_empty());
1428 assert_eq!(
1429 errors[0].message(),
1430 "custom constraint 'port_range: port 80 must be between 1024 and 65535' failed for value '80'"
1431 );
1432 }
1433
1434 #[test]
1435 fn test_custom_schema_multiple_validators() {
1436 let custom_schema = CustomSchema::new("config-validation")
1438 .allow_types(&[ScalarType::String, ScalarType::Integer])
1439 .with_validator(ScalarType::String, |value, _path| {
1440 if value.len() >= 3 {
1441 CustomValidationResult::Valid
1442 } else {
1443 CustomValidationResult::invalid(
1444 "string_length",
1445 format!("string too short: '{}'", value),
1446 )
1447 }
1448 })
1449 .with_validator(ScalarType::Integer, |value, _path| {
1450 if let Ok(num) = value.parse::<i32>() {
1451 if num >= 0 {
1452 CustomValidationResult::Valid
1453 } else {
1454 CustomValidationResult::invalid(
1455 "negative_number",
1456 format!("negative numbers not allowed: {}", num),
1457 )
1458 }
1459 } else {
1460 CustomValidationResult::invalid(
1461 "integer_format",
1462 format!("invalid integer: {}", value),
1463 )
1464 }
1465 });
1466
1467 let validator = SchemaValidator::custom(custom_schema);
1468
1469 let valid_yaml = r#"
1471name: "valid-name"
1472count: 100
1473"#;
1474 let valid_doc = create_test_document(valid_yaml);
1475 assert!(validator.validate(&valid_doc).is_ok());
1476
1477 let invalid_yaml = r#"
1479name: "ab"
1480count: 100
1481"#;
1482 let invalid_doc = create_test_document(invalid_yaml);
1483 let result = validator.validate(&invalid_doc);
1484 assert!(result.is_err());
1485
1486 let errors = result.unwrap_err();
1487 assert_eq!(errors.len(), 1);
1488 assert_eq!(
1489 errors[0].message(),
1490 "custom constraint 'string_length: string too short: 'ab'' failed for value 'ab'"
1491 );
1492 }
1493
1494 #[test]
1495 fn test_custom_schema_strict_mode() {
1496 let custom_schema = CustomSchema::new("strict-test")
1497 .allow_type(ScalarType::String)
1498 .strict();
1499
1500 let validator = SchemaValidator::custom(custom_schema);
1501
1502 let yaml_with_int = r#"
1504value: 42
1505"#;
1506 let doc = create_test_document(yaml_with_int);
1507 let result = validator.validate(&doc);
1508 assert!(result.is_err());
1509
1510 let errors = result.unwrap_err();
1511 assert_eq!(errors.len(), 1);
1512 assert_eq!(
1513 errors[0].message(),
1514 "type Integer not allowed in strict-test schema, expected one of [String]"
1515 );
1516 }
1517
1518 #[test]
1519 fn test_custom_schema_name() {
1520 let custom_schema = CustomSchema::new("my-custom-schema").allow_type(ScalarType::String);
1521 let schema = Schema::Custom(custom_schema);
1522
1523 assert_eq!(schema.name(), "my-custom-schema");
1524 }
1525}