Skip to main content

yaml_edit/
schema.rs

1//! YAML Schema validation support
2//!
3//! This module provides schema validation for YAML documents, supporting
4//! the standard YAML schemas: Failsafe, JSON, and Core.
5
6use crate::scalar::{ScalarType, ScalarValue};
7use crate::yaml::{Document, Mapping, Scalar, Sequence, TaggedNode};
8
9/// Specific type of validation error that occurred
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum ValidationErrorKind {
12    /// A scalar type is not allowed in the current schema
13    TypeNotAllowed {
14        /// The scalar type that was found
15        found_type: ScalarType,
16        /// The types that are allowed in this schema
17        allowed_types: Vec<ScalarType>,
18    },
19    /// Custom validation constraint failed
20    CustomConstraintFailed {
21        /// The constraint that failed
22        constraint_name: String,
23        /// The actual value that failed validation
24        actual_value: String,
25    },
26    /// Type coercion failed
27    CoercionFailed {
28        /// The type that was found
29        from_type: ScalarType,
30        /// The types that coercion was attempted to
31        to_types: Vec<ScalarType>,
32    },
33}
34
35/// Error that occurs during schema validation
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ValidationError {
38    /// The specific kind of validation error
39    pub kind: ValidationErrorKind,
40    /// Path to the node that failed validation (e.g., "root.items\[0\].name")
41    pub path: String,
42    /// Name of the schema that was being validated against
43    pub schema_name: String,
44}
45
46impl ValidationError {
47    /// Create a new type not allowed error
48    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    /// Create a new custom constraint failed error
65    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    /// Create a new coercion failed error
82    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    /// Get a human-readable error message
99    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
137/// Result type for schema validation operations
138pub type ValidationResult<T> = Result<T, Vec<ValidationError>>;
139
140/// Result of a custom validation function
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub enum CustomValidationResult {
143    /// Validation passed
144    Valid,
145    /// Validation failed with a specific constraint name and reason
146    Invalid {
147        /// Name of the constraint that failed (e.g., "email_format", "port_range")
148        constraint: String,
149        /// Human-readable reason for failure
150        reason: String,
151    },
152}
153
154impl CustomValidationResult {
155    /// Create a validation failure
156    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    /// Check if validation passed
164    pub fn is_valid(&self) -> bool {
165        matches!(self, Self::Valid)
166    }
167
168    /// Check if validation failed
169    pub fn is_invalid(&self) -> bool {
170        matches!(self, Self::Invalid { .. })
171    }
172}
173
174/// Custom validation function for scalar values
175///
176/// Takes (value, path) and returns CustomValidationResult
177pub type CustomValidator = Box<dyn Fn(&str, &str) -> CustomValidationResult + Send + Sync>;
178
179/// Custom schema definition with user-defined validation rules
180pub struct CustomSchema {
181    /// Schema name for error messages
182    pub name: String,
183    /// Allowed scalar types in this schema
184    pub allowed_types: Vec<ScalarType>,
185    /// Custom validation functions by type
186    pub custom_validators: std::collections::HashMap<ScalarType, CustomValidator>,
187    /// Whether to allow type coercion
188    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    /// Create a new custom schema
207    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    /// Allow a specific scalar type
217    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    /// Allow multiple scalar types
225    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    /// Add a custom validator for a specific type
235    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    /// Disable type coercion for strict validation
245    pub fn strict(mut self) -> Self {
246        self.allow_coercion = false;
247        self
248    }
249
250    /// Check if a scalar type is allowed
251    pub fn allows_type(&self, scalar_type: ScalarType) -> bool {
252        self.allowed_types.contains(&scalar_type)
253    }
254
255    /// Validate a scalar value with custom rules
256    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        // Check if type is allowed
261        if !self.allows_type(scalar_type) {
262            if self.allow_coercion {
263                // Try coercion to allowed types
264                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        // Run custom validator if present
287        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/// YAML Schema types as defined in YAML 1.2 specification
304#[derive(Debug)]
305pub enum Schema {
306    /// Failsafe schema - only strings, sequences, and mappings
307    Failsafe,
308    /// JSON schema - JSON-compatible types only
309    Json,
310    /// Core schema - full YAML 1.2 type system
311    Core,
312    /// User-defined custom schema
313    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    /// Get the name of this schema
330    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    /// Check if a scalar type is allowed in this schema
340    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, // Core schema allows all types
352            Schema::Custom(custom) => custom.allows_type(scalar_type),
353        }
354    }
355
356    /// Get the allowed scalar types for this schema
357    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/// Schema validator for YAML documents
384#[derive(Debug)]
385pub struct SchemaValidator {
386    schema: Schema,
387    strict: bool,
388}
389
390impl SchemaValidator {
391    /// Create a new schema validator
392    pub fn new(schema: Schema) -> Self {
393        Self {
394            schema,
395            strict: false,
396        }
397    }
398
399    /// Create a failsafe schema validator
400    pub fn failsafe() -> Self {
401        Self::new(Schema::Failsafe)
402    }
403
404    /// Create a JSON schema validator  
405    pub fn json() -> Self {
406        Self::new(Schema::Json)
407    }
408
409    /// Create a core schema validator
410    pub fn core() -> Self {
411        Self::new(Schema::Core)
412    }
413
414    /// Create a validator for a custom schema
415    pub fn custom(schema: CustomSchema) -> Self {
416        Self::new(Schema::Custom(schema))
417    }
418
419    /// Enable strict mode - disallow type coercion
420    pub fn strict(mut self) -> Self {
421        self.strict = true;
422        self
423    }
424
425    /// Get the schema type
426    pub fn schema(&self) -> &Schema {
427        &self.schema
428    }
429
430    /// Validate a YAML document against the schema
431    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    /// Validate a document node
443    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        // If none match, it might be empty or null - that's generally allowed
457    }
458
459    /// Validate a scalar value
460    fn validate_scalar(&self, scalar: &Scalar, path: &str, errors: &mut Vec<ValidationError>) {
461        let content = scalar.as_string();
462
463        // Handle custom schema validation differently
464        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        // Standard schema validation
472        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            // Try type coercion if not in strict mode
477            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    /// Validate a sequence
508    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    /// Validate a mapping  
516    fn validate_mapping(&self, map: &Mapping, path: &str, errors: &mut Vec<ValidationError>) {
517        for (key_node, value_node) in map.pairs() {
518            // Get the key name; KEY node wraps the actual content
519            let key_name = key_node.text().to_string().trim().to_string();
520
521            // Keys in YAML are typically strings and don't need schema validation
522            // The schema applies to the values, not the keys
523            let value_path = format!("{path}.{key_name}");
524            self.validate_node(&value_node, &value_path, errors);
525        }
526    }
527
528    /// Validate a syntax node (could be scalar, sequence, or mapping)
529    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        // Use smart extraction to handle wrapper nodes automatically
538        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        // If none match, it might be a different node type - skip validation
548    }
549
550    /// Validate a tagged scalar value (e.g., !!timestamp, !!regex)
551    fn validate_tagged_node(
552        &self,
553        tagged_node: &TaggedNode,
554        path: &str,
555        errors: &mut Vec<ValidationError>,
556    ) {
557        // Handle custom schema validation
558        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        // Standard tagged scalar validation
567        let scalar_type = self.get_tagged_node_type(tagged_node);
568
569        if !self.schema.allows_scalar_type(scalar_type) {
570            // For tagged scalars, we can't coerce them to other types since they have explicit type information
571            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    /// Determine the scalar type from a tagged scalar
581    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    /// Check if a document can be coerced to match the schema
596    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    /// Check if a document can be coerced to match the schema
612    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                // Try to coerce to an allowed type
619                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            // Recursively check sequence items for coercion
635            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            // Recursively check mapping key-value pairs for coercion
641            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    /// Check coercion for a generic syntax node
650    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        // Use smart extraction to handle wrapper nodes automatically
659        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                // Tagged scalars generally can't be coerced since they have explicit type info
683                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        // Should fail because age (integer) and active (boolean) are not allowed in failsafe schema
863        assert!(result.is_err());
864        let errors = result.unwrap_err();
865        assert!(!errors.is_empty());
866
867        // Should have errors for integer and boolean types
868        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        // Debug: Check what types are detected
886        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        // Should fail because timestamp and regex are not allowed in JSON schema
920        assert!(result.is_err());
921        let errors = result.unwrap_err();
922        assert!(!errors.is_empty());
923
924        // Should have errors for timestamp and regex types
925        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        // Test with unquoted integer (should fail in failsafe strict mode)
934        let yaml_str = r#"
935count: 42
936active: true
937"#;
938        let document = create_test_document(yaml_str);
939
940        // Non-strict failsafe should pass via coercion
941        let validator = SchemaValidator::failsafe();
942        assert!(validator.can_coerce(&document).is_ok());
943
944        // Strict failsafe should fail (integers and booleans not allowed)
945        let strict_validator = SchemaValidator::failsafe().strict();
946        let result = strict_validator.validate(&document);
947        assert!(result.is_err());
948
949        // Test with actual string (should pass in both modes)
950        let string_yaml = r#"
951name: hello
952message: world
953"#;
954        let string_document = create_test_document(string_yaml);
955
956        // Both should pass since these are actual strings
957        let non_strict_result = validator.validate(&string_document);
958        let strict_result = strict_validator.validate(&string_document);
959
960        // Debug: Check what types are detected for plain strings
961        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        // Note: Due to current type inference limitations, quoted numbers like "42"
984        // are detected as integers rather than strings. This is a limitation of
985        // the ScalarValue::parse() function, not the schema validation logic.
986        // For now, we test with unambiguous strings.
987        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            // Check that error paths are meaningful and start with "root"
1006            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        // Core includes at minimum: String, Integer, Float, Boolean, Null, Timestamp, Regex
1037        // (plus Binary if the base64 feature is enabled)
1038        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        // Failsafe should fail for non-string types in the sequence
1064        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        // JSON should succeed since all types are JSON-compatible
1071        let json_validator = SchemaValidator::json();
1072        let result = json_validator.validate(&document);
1073        assert!(result.is_ok());
1074
1075        // Core should succeed since all types are allowed
1076        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        // Failsafe should fail due to nested integers and booleans
1096        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        // Check that errors have meaningful paths
1102        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        // JSON should succeed
1107        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        // Failsafe should fail
1125        let failsafe_validator = SchemaValidator::failsafe().strict();
1126        let result = failsafe_validator.validate(&document);
1127        assert!(result.is_err());
1128
1129        // JSON should fail
1130        let json_validator = SchemaValidator::json().strict();
1131        let result = json_validator.validate(&document);
1132        assert!(result.is_err());
1133
1134        // Core should succeed
1135        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        // Non-strict JSON validation should pass via coercion
1153        let json_validator = SchemaValidator::json();
1154        let result = json_validator.can_coerce(&document);
1155        assert!(result.is_ok());
1156
1157        // Strict JSON validation should fail (strings are not the exact types)
1158        let strict_json_validator = SchemaValidator::json().strict();
1159        let result = strict_json_validator.validate(&document);
1160        // Actually strings are allowed in JSON schema, so this should pass
1161        assert!(result.is_ok());
1162
1163        // But if we put non-JSON types, strict mode should fail
1164        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        // Check that we have errors with proper path information
1195        let paths: Vec<&str> = errors.iter().map(|e| e.path.as_str()).collect();
1196
1197        // Should have paths that show the nested structure
1198        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        // Print paths for debugging if needed
1203        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        // Test that our schemas match YAML 1.2 specification requirements
1211
1212        // 1. Failsafe Schema - Should only allow strings, mappings, and sequences
1213        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        // 2. JSON Schema - Should allow JSON-compatible types
1222        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)); // Not in JSON
1229        assert!(!json.allows_scalar_type(ScalarType::Regex)); // Not in JSON
1230        #[cfg(feature = "base64")]
1231        assert!(!json.allows_scalar_type(ScalarType::Binary)); // Not in JSON
1232
1233        // 3. Core Schema - Should allow all YAML types
1234        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        // Test schema names match spec
1246        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        // Examples from YAML 1.2 specification
1254
1255        // Failsafe: Should accept plain strings but reject typed values
1256        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        // Non-strict mode allows coercion
1263        assert!(failsafe_validator.validate(&failsafe_doc).is_ok());
1264
1265        // JSON: Should accept JSON-compatible types
1266        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        // Core: Should accept all YAML types including timestamps
1278        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        // JSON should reject YAML-specific types
1288        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        // Create a custom schema that only allows strings and integers (strict mode to prevent coercion)
1295        let custom_schema = CustomSchema::new("test")
1296            .allow_types(&[ScalarType::String, ScalarType::Integer])
1297            .strict(); // No coercion
1298
1299        let validator = SchemaValidator::custom(custom_schema);
1300
1301        // Test with allowed types
1302        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        // Test with disallowed type
1316        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        // Create a custom schema with string validators
1335        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        // Test with valid email
1348        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        // Test with invalid email
1355        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        // Custom schema with integer range validation
1373        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        // Test with valid port
1396        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        // Test with invalid port (too low)
1403        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        // Schema with multiple type validators
1421        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        // Test with valid values
1454        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        // Test with invalid string (too short)
1462        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        // Test that even valid integers are rejected in strict mode
1487        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}