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