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