Skip to main content

regorus/schema/
validate.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4#![allow(missing_debug_implementations)] // validator is zero-sized marker
5#![allow(dead_code)]
6#![allow(clippy::pattern_type_mismatch, clippy::needless_continue)]
7
8use crate::{
9    schema::{error::ValidationError, Schema, Type},
10    *,
11};
12use alloc::collections::BTreeMap;
13use regex::Regex;
14
15type String = Rc<str>;
16
17/// Validator for checking if a Value conforms to a Schema.
18pub struct SchemaValidator;
19
20impl SchemaValidator {
21    /// Validates a Value against a Schema.
22    ///
23    /// # Arguments
24    /// * `value` - The Value to validate
25    /// * `schema` - The Schema to validate against
26    ///
27    /// # Returns
28    /// * `Ok(())` if the value conforms to the schema
29    /// * `Err(ValidationError)` if validation fails
30    ///
31    /// # Example
32    /// ```rust
33    /// use regorus::schema::{Schema, validate::SchemaValidator};
34    /// use regorus::Value;
35    /// use serde_json::json;
36    ///
37    /// let schema_json = json!({
38    ///     "type": "string",
39    ///     "minLength": 1,
40    ///     "maxLength": 10
41    /// });
42    /// let schema = Schema::from_serde_json_value(schema_json).unwrap();
43    /// let value = Value::from("hello");
44    ///
45    /// let result = SchemaValidator::validate(&value, &schema);
46    /// assert!(result.is_ok());
47    /// ```
48    pub fn validate(value: &Value, schema: &Schema) -> Result<(), ValidationError> {
49        Self::validate_with_path(value, schema, "")
50    }
51
52    /// Internal validation function that tracks the current path for error reporting.
53    fn validate_with_path(
54        value: &Value,
55        schema: &Schema,
56        path: &str,
57    ) -> Result<(), ValidationError> {
58        match schema.as_type() {
59            Type::Any { .. } => {
60                // Any type accepts all values
61                Ok(())
62            }
63            Type::Integer {
64                minimum, maximum, ..
65            } => Self::validate_integer(value, *minimum, *maximum, path),
66            Type::Number {
67                minimum, maximum, ..
68            } => Self::validate_number(value, *minimum, *maximum, path),
69            Type::Boolean { .. } => Self::validate_boolean(value, path),
70            Type::Null { .. } => Self::validate_null(value, path),
71            Type::String {
72                min_length,
73                max_length,
74                pattern,
75                ..
76            } => Self::validate_string(value, *min_length, *max_length, pattern.as_ref(), path),
77            Type::Array {
78                items,
79                min_items,
80                max_items,
81                ..
82            } => Self::validate_array(value, items, *min_items, *max_items, path),
83            Type::Object {
84                properties,
85                required,
86                additional_properties,
87                discriminated_subobject,
88                ..
89            } => Self::validate_object(
90                value,
91                properties,
92                required.as_ref().map(|r| &**r),
93                additional_properties.as_ref(),
94                discriminated_subobject.as_ref().map(|d| &**d),
95                path,
96            ),
97            Type::AnyOf(schemas) => Self::validate_any_of(value, schemas, path),
98            Type::Const {
99                value: const_value, ..
100            } => Self::validate_const(value, const_value, path),
101            Type::Enum { values, .. } => Self::validate_enum(value, values, path),
102            Type::Set { items, .. } => Self::validate_set(value, items, path),
103        }
104    }
105
106    fn validate_integer(
107        value: &Value,
108        minimum: Option<i64>,
109        maximum: Option<i64>,
110        path: &str,
111    ) -> Result<(), ValidationError> {
112        match value {
113            Value::Number(num) => {
114                if let Some(int_val) = num.as_i64() {
115                    if let Some(min) = minimum {
116                        if int_val < min {
117                            return Err(ValidationError::OutOfRange {
118                                value: int_val.to_string().into(),
119                                min: Some(min.to_string().into()),
120                                max: maximum.map(|m| m.to_string().into()),
121                                path: path.to_string().into(),
122                            });
123                        }
124                    }
125                    if let Some(max) = maximum {
126                        if int_val > max {
127                            return Err(ValidationError::OutOfRange {
128                                value: int_val.to_string().into(),
129                                min: minimum.map(|m| m.to_string().into()),
130                                max: Some(max.to_string().into()),
131                                path: path.into(),
132                            });
133                        }
134                    }
135                    Ok(())
136                } else {
137                    Err(ValidationError::TypeMismatch {
138                        expected: "integer".into(),
139                        actual: "non-integer number".into(),
140                        path: path.into(),
141                    })
142                }
143            }
144            _ => Err(ValidationError::TypeMismatch {
145                expected: "integer".into(),
146                actual: Self::value_type_name(value),
147                path: path.into(),
148            }),
149        }
150    }
151
152    fn validate_number(
153        value: &Value,
154        minimum: Option<f64>,
155        maximum: Option<f64>,
156        path: &str,
157    ) -> Result<(), ValidationError> {
158        match value {
159            Value::Number(num) => {
160                // Try to get a precise f64 representation for range checking
161                if let Some(float_val) = num.as_f64() {
162                    if let Some(min) = minimum {
163                        if float_val < min {
164                            return Err(ValidationError::OutOfRange {
165                                value: float_val.to_string().into(),
166                                min: Some(min.to_string().into()),
167                                max: maximum.map(|m| m.to_string().into()),
168                                path: path.into(),
169                            });
170                        }
171                    }
172                    if let Some(max) = maximum {
173                        if float_val > max {
174                            return Err(ValidationError::OutOfRange {
175                                value: float_val.to_string().into(),
176                                min: minimum.map(|m| m.to_string().into()),
177                                max: Some(max.to_string().into()),
178                                path: path.to_string().into(),
179                            });
180                        }
181                    }
182                    Ok(())
183                } else {
184                    // For large integers that exceed f64's safe integer range,
185                    // use integer-based comparison when possible.
186                    // This handles values like 1e18 (1000000000000000000) which are valid
187                    // numbers but cannot be precisely represented as f64.
188                    if let Some(int_val) = num.as_i64() {
189                        // Integer fits in i64 - use integer comparison
190                        if let Some(min) = minimum {
191                            if (int_val as f64) < min {
192                                return Err(ValidationError::OutOfRange {
193                                    value: int_val.to_string().into(),
194                                    min: Some(min.to_string().into()),
195                                    max: maximum.map(|m| m.to_string().into()),
196                                    path: path.into(),
197                                });
198                            }
199                        }
200                        if let Some(max) = maximum {
201                            if (int_val as f64) > max {
202                                return Err(ValidationError::OutOfRange {
203                                    value: int_val.to_string().into(),
204                                    min: minimum.map(|m| m.to_string().into()),
205                                    max: Some(max.to_string().into()),
206                                    path: path.into(),
207                                });
208                            }
209                        }
210                        Ok(())
211                    } else if let Some(big_val) = num.as_big() {
212                        // Large integer (BigInt) - check range using BigInt comparison
213                        if let Some(min) = minimum {
214                            let min_int = min as i128;
215                            if let Some(val_i128) = num_traits::ToPrimitive::to_i128(&*big_val) {
216                                if val_i128 < min_int {
217                                    return Err(ValidationError::OutOfRange {
218                                        value: big_val.to_string().into(),
219                                        min: Some(min.to_string().into()),
220                                        max: maximum.map(|m| m.to_string().into()),
221                                        path: path.into(),
222                                    });
223                                }
224                            }
225                            // If we can't convert to i128, the value is extremely large
226                            // and likely exceeds any reasonable minimum bound
227                        }
228                        if let Some(max) = maximum {
229                            let max_int = max as i128;
230                            if let Some(val_i128) = num_traits::ToPrimitive::to_i128(&*big_val) {
231                                if val_i128 > max_int {
232                                    return Err(ValidationError::OutOfRange {
233                                        value: big_val.to_string().into(),
234                                        min: minimum.map(|m| m.to_string().into()),
235                                        max: Some(max.to_string().into()),
236                                        path: path.into(),
237                                    });
238                                }
239                            }
240                            // If we can't convert to i128, the value exceeds any f64 max bound
241                        }
242                        Ok(())
243                    } else {
244                        Err(ValidationError::TypeMismatch {
245                            expected: "number".into(),
246                            actual: "non-numeric value".into(),
247                            path: path.into(),
248                        })
249                    }
250                }
251            }
252            _ => Err(ValidationError::TypeMismatch {
253                expected: "number".into(),
254                actual: Self::value_type_name(value),
255                path: path.into(),
256            }),
257        }
258    }
259
260    fn validate_boolean(value: &Value, path: &str) -> Result<(), ValidationError> {
261        match value {
262            Value::Bool(_) => Ok(()),
263            _ => Err(ValidationError::TypeMismatch {
264                expected: "boolean".into(),
265                actual: Self::value_type_name(value),
266                path: path.into(),
267            }),
268        }
269    }
270
271    fn validate_null(value: &Value, path: &str) -> Result<(), ValidationError> {
272        match value {
273            Value::Null => Ok(()),
274            _ => Err(ValidationError::TypeMismatch {
275                expected: "null".into(),
276                actual: Self::value_type_name(value),
277                path: path.into(),
278            }),
279        }
280    }
281
282    fn validate_string(
283        value: &Value,
284        min_length: Option<usize>,
285        max_length: Option<usize>,
286        pattern: Option<&String>,
287        path: &str,
288    ) -> Result<(), ValidationError> {
289        match value {
290            Value::String(string_value) => {
291                let str_len = string_value.len();
292
293                // Check length constraints
294                if let Some(min) = min_length {
295                    if str_len < min {
296                        return Err(ValidationError::LengthConstraint {
297                            actual_length: str_len,
298                            min_length: Some(min),
299                            max_length,
300                            path: path.into(),
301                        });
302                    }
303                }
304                if let Some(max) = max_length {
305                    if str_len > max {
306                        return Err(ValidationError::LengthConstraint {
307                            actual_length: str_len,
308                            min_length,
309                            max_length: Some(max),
310                            path: path.into(),
311                        });
312                    }
313                }
314
315                // Check pattern constraint
316                if let Some(pattern_str) = pattern {
317                    let regex =
318                        Regex::new(pattern_str).map_err(|e| ValidationError::InvalidPattern {
319                            pattern: pattern_str.as_ref().into(),
320                            error: e.to_string().into(),
321                        })?;
322
323                    if !regex.is_match(string_value) {
324                        return Err(ValidationError::PatternMismatch {
325                            value: string_value.to_string().into(),
326                            pattern: pattern_str.clone(),
327                            path: path.into(),
328                        });
329                    }
330                }
331
332                Ok(())
333            }
334            _ => Err(ValidationError::TypeMismatch {
335                expected: "string".into(),
336                actual: Self::value_type_name(value),
337                path: path.into(),
338            }),
339        }
340    }
341
342    fn validate_array(
343        value: &Value,
344        items_schema: &Schema,
345        min_items: Option<usize>,
346        max_items: Option<usize>,
347        path: &str,
348    ) -> Result<(), ValidationError> {
349        match value {
350            Value::Array(array_value) => {
351                let arr_len = array_value.len();
352
353                // Check size constraints
354                if let Some(min) = min_items {
355                    if arr_len < min {
356                        return Err(ValidationError::ArraySizeConstraint {
357                            actual_size: arr_len,
358                            min_items: Some(min),
359                            max_items,
360                            path: path.into(),
361                        });
362                    }
363                }
364                if let Some(max) = max_items {
365                    if arr_len > max {
366                        return Err(ValidationError::ArraySizeConstraint {
367                            actual_size: arr_len,
368                            min_items,
369                            max_items: Some(max),
370                            path: path.into(),
371                        });
372                    }
373                }
374
375                // Validate each item
376                for (index, item) in array_value.iter().enumerate() {
377                    Self::validate_with_path(
378                        item,
379                        items_schema,
380                        &if path.is_empty() {
381                            format!("[{index}]")
382                        } else {
383                            format!("{path}[{index}]")
384                        },
385                    )
386                    .map_err(|e| {
387                        ValidationError::ArrayItemValidationFailed {
388                            index,
389                            path: path.into(),
390                            error: Box::new(e),
391                        }
392                    })?;
393                }
394
395                Ok(())
396            }
397            _ => Err(ValidationError::TypeMismatch {
398                expected: "array".into(),
399                actual: Self::value_type_name(value),
400                path: path.into(),
401            }),
402        }
403    }
404
405    fn validate_object(
406        value: &Value,
407        properties: &BTreeMap<String, Schema>,
408        required: Option<&Vec<String>>,
409        additional_properties: Option<&Schema>,
410        discriminated_subobject: Option<&crate::schema::DiscriminatedSubobject>,
411        path: &str,
412    ) -> Result<(), ValidationError> {
413        match value {
414            Value::Object(object_value) => {
415                // Check required properties
416                if let Some(required_props) = required {
417                    for required_prop in required_props.iter() {
418                        if !object_value.contains_key(&Value::String(required_prop.clone())) {
419                            return Err(ValidationError::MissingRequiredProperty {
420                                property: required_prop.clone(),
421                                path: path.into(),
422                            });
423                        }
424                    }
425                }
426
427                // Handle discriminated subobjects (allOf with if/then)
428                // Validates against the appropriate variant schema based on discriminator field value
429                if let Some(discriminated_subobject) = discriminated_subobject {
430                    Self::validate_discriminated_subobject_with_base(
431                        object_value,
432                        discriminated_subobject,
433                        properties,
434                        additional_properties,
435                        path,
436                    )?;
437                } else {
438                    // Only validate regular object properties if no discriminated subobject exists
439                    // Validate each property
440                    for (prop_name, prop_value) in object_value.iter() {
441                        // First, ensure the property key is a string
442                        let prop_name_str = match prop_name {
443                            Value::String(string_key) => string_key,
444                            _ => {
445                                return Err(ValidationError::NonStringKey {
446                                    key_type: Self::value_type_name(prop_name),
447                                    path: path.into(),
448                                });
449                            }
450                        };
451
452                        // Create property path lazily using a closure
453                        let make_prop_path = || {
454                            if path.is_empty() {
455                                format!("[{prop_name_str}]")
456                            } else {
457                                format!("{path}.{prop_name_str}")
458                            }
459                        };
460
461                        if let Some(prop_schema) = properties.get(prop_name_str) {
462                            // Property is defined in schema, validate against it
463                            Self::validate_with_path(prop_value, prop_schema, &make_prop_path())
464                                .map_err(|e| ValidationError::PropertyValidationFailed {
465                                    property: prop_name_str.clone(),
466                                    path: path.into(),
467                                    error: Box::new(e),
468                                })?;
469                        } else if let Some(additional_schema) = additional_properties {
470                            // Property is not defined but additional properties are allowed
471                            Self::validate_with_path(
472                                prop_value,
473                                additional_schema,
474                                &make_prop_path(),
475                            )
476                            .map_err(|e| {
477                                ValidationError::PropertyValidationFailed {
478                                    property: prop_name_str.clone(),
479                                    path: path.into(),
480                                    error: Box::new(e),
481                                }
482                            })?;
483                        } else {
484                            // Property is not defined and additional properties are not allowed
485                            return Err(ValidationError::AdditionalPropertiesNotAllowed {
486                                property: prop_name_str.clone(),
487                                path: path.into(),
488                            });
489                        }
490                    }
491                }
492
493                Ok(())
494            }
495            _ => Err(ValidationError::TypeMismatch {
496                expected: "object".into(),
497                actual: Self::value_type_name(value),
498                path: path.into(),
499            }),
500        }
501    }
502
503    fn validate_any_of(
504        value: &Value,
505        schemas: &Vec<Schema>,
506        path: &str,
507    ) -> Result<(), ValidationError> {
508        let mut errors = Vec::new();
509
510        for schema in schemas {
511            match Self::validate_with_path(value, schema, path) {
512                Ok(()) => return Ok(()), // If any schema matches, validation succeeds
513                Err(e) => errors.push(e),
514            }
515        }
516
517        // If no schema matched, return error with all validation attempts
518        Err(ValidationError::NoUnionMatch {
519            path: path.into(),
520            errors,
521        })
522    }
523
524    fn validate_const(
525        value: &Value,
526        const_value: &Value,
527        path: &str,
528    ) -> Result<(), ValidationError> {
529        if value == const_value {
530            Ok(())
531        } else {
532            let expected_json =
533                serde_json::to_string(const_value).unwrap_or_else(|_| format!("{const_value:?}"));
534            let actual_json = serde_json::to_string(value).unwrap_or_else(|_| format!("{value:?}"));
535
536            Err(ValidationError::ConstMismatch {
537                expected: expected_json.into(),
538                actual: actual_json.into(),
539                path: path.into(),
540            })
541        }
542    }
543
544    fn validate_enum(
545        value: &Value,
546        allowed_values: &[Value],
547        path: &str,
548    ) -> Result<(), ValidationError> {
549        if allowed_values.contains(value) {
550            Ok(())
551        } else {
552            // Convert Value to JSON string, fallback to debug format if JSON serialization fails
553            let value_json = serde_json::to_string(value).unwrap_or_else(|_| format!("{value:?}"));
554
555            let allowed_json: Vec<String> = allowed_values
556                .iter()
557                .map(|v| {
558                    serde_json::to_string(v)
559                        .unwrap_or_else(|_| format!("{v:?}"))
560                        .into()
561                })
562                .collect();
563
564            Err(ValidationError::NotInEnum {
565                value: value_json.into(),
566                allowed_values: allowed_json,
567                path: path.into(),
568            })
569        }
570    }
571
572    fn validate_set(
573        value: &Value,
574        items_schema: &Schema,
575        path: &str,
576    ) -> Result<(), ValidationError> {
577        match value {
578            Value::Set(set_value) => {
579                // Validate each item in the set
580                for (index, item) in set_value.iter().enumerate() {
581                    Self::validate_with_path(
582                        item,
583                        items_schema,
584                        &if path.is_empty() {
585                            format!("{{{index}}}]")
586                        } else {
587                            format!("{path}{{{index}}}]")
588                        },
589                    )?;
590                }
591                Ok(())
592            }
593            _ => Err(ValidationError::TypeMismatch {
594                expected: "set".into(),
595                actual: Self::value_type_name(value),
596                path: path.into(),
597            }),
598        }
599    }
600
601    fn validate_discriminated_subobject_with_base(
602        object_value: &BTreeMap<Value, Value>,
603        discriminated_subobject: &crate::schema::DiscriminatedSubobject,
604        base_properties: &BTreeMap<String, Schema>,
605        base_additional_properties: Option<&Schema>,
606        path: &str,
607    ) -> Result<(), ValidationError> {
608        let discriminator_field = &discriminated_subobject.discriminator;
609        let discriminator_key = Value::String(discriminator_field.clone());
610
611        // Find the discriminator field value in the object
612        let discriminator_value = object_value.get(&discriminator_key).ok_or_else(|| {
613            ValidationError::MissingDiscriminator {
614                discriminator: discriminator_field.clone(),
615                path: path.into(),
616            }
617        })?;
618
619        // Extract the string value from the discriminator field
620        let discriminator_str = match discriminator_value {
621            Value::String(string_value) => string_value.as_ref(),
622            _ => {
623                return Err(ValidationError::TypeMismatch {
624                    expected: "string".into(),
625                    actual: Self::value_type_name(discriminator_value),
626                    path: format!("{path}.{discriminator_field}").into(),
627                });
628            }
629        };
630
631        // Find the corresponding variant schema
632        let variant_schema = discriminated_subobject
633            .variants
634            .get(discriminator_str)
635            .ok_or_else(|| ValidationError::UnknownDiscriminatorValue {
636                discriminator: discriminator_field.clone(),
637                value: discriminator_str.into(),
638                allowed_values: discriminated_subobject.variants.keys().cloned().collect(),
639                path: path.into(),
640            })?;
641
642        // Validate all properties against the appropriate schemas
643        for (prop_name, prop_value) in object_value.iter() {
644            // First, ensure the property key is a string
645            let prop_name_str = match prop_name {
646                Value::String(string_key) => string_key,
647                _ => {
648                    return Err(ValidationError::NonStringKey {
649                        key_type: Self::value_type_name(prop_name),
650                        path: path.into(),
651                    });
652                }
653            };
654
655            // Create property path lazily using a closure
656            let make_prop_path = || {
657                if path.is_empty() {
658                    format!("[{prop_name_str}]")
659                } else {
660                    format!("{path}.{prop_name_str}")
661                }
662            };
663
664            // Check if this property is defined in the variant schema first
665            if variant_schema.properties.get(prop_name_str).is_some() {
666                // Validate later in subobject.
667                continue;
668            }
669
670            // Check if this property is defined in the base schema properties
671            if let Some(prop_schema) = base_properties.get(prop_name_str) {
672                // Property is defined in base schema, validate against it
673                Self::validate_with_path(prop_value, prop_schema, &make_prop_path()).map_err(
674                    |e| ValidationError::PropertyValidationFailed {
675                        property: prop_name_str.clone(),
676                        path: path.into(),
677                        error: Box::new(e),
678                    },
679                )?;
680                continue;
681            }
682
683            // Check if additional properties are allowed in the variant
684            if variant_schema.additional_properties.is_some() {
685                // Property is not defined but additional properties are allowed in variant.
686                // Validate later.
687                continue;
688            } else if let Some(base_additional) = base_additional_properties {
689                // Check if additional properties are allowed in the base schema
690                Self::validate_with_path(prop_value, base_additional, &make_prop_path()).map_err(
691                    |e| ValidationError::PropertyValidationFailed {
692                        property: prop_name_str.clone(),
693                        path: path.into(),
694                        error: Box::new(e),
695                    },
696                )?;
697            } else {
698                // Property is not defined and additional properties are not allowed
699                return Err(ValidationError::AdditionalPropertiesNotAllowed {
700                    property: prop_name_str.clone(),
701                    path: path.into(),
702                });
703            }
704        }
705
706        // Validate the object against the variant schema for required properties
707        Self::validate_subobject(object_value, variant_schema, path).map_err(|e| {
708            ValidationError::DiscriminatedSubobjectValidationFailed {
709                discriminator: discriminator_field.clone(),
710                value: discriminator_str.into(),
711                path: path.into(),
712                error: Box::new(e),
713            }
714        })
715    }
716
717    fn validate_subobject(
718        object_value: &BTreeMap<Value, Value>,
719        subobject: &crate::schema::Subobject,
720        path: &str,
721    ) -> Result<(), ValidationError> {
722        // Check required properties from the subobject
723        if let Some(required_props) = &subobject.required {
724            for required_prop in required_props.iter() {
725                if !object_value.contains_key(&Value::String(required_prop.clone())) {
726                    return Err(ValidationError::MissingRequiredProperty {
727                        property: required_prop.clone(),
728                        path: path.into(),
729                    });
730                }
731            }
732        }
733
734        // Validate each property in the subobject
735        for (prop_name, prop_schema) in subobject.properties.iter() {
736            let prop_key = Value::String(prop_name.clone());
737            if let Some(prop_value) = object_value.get(&prop_key) {
738                Self::validate_with_path(
739                    prop_value,
740                    prop_schema,
741                    &if path.is_empty() {
742                        format!("[{prop_name}]")
743                    } else {
744                        format!("{path}.{prop_name}")
745                    },
746                )
747                .map_err(|e| ValidationError::PropertyValidationFailed {
748                    property: prop_name.clone(),
749                    path: path.into(),
750                    error: Box::new(e),
751                })?;
752            }
753        }
754
755        // Handle additional properties if specified
756        if let Some(additional_schema) = &subobject.additional_properties {
757            for (prop_name, prop_value) in object_value.iter() {
758                if let Value::String(prop_name_str) = prop_name {
759                    if !subobject.properties.contains_key(prop_name_str) {
760                        Self::validate_with_path(
761                            prop_value,
762                            additional_schema,
763                            &if path.is_empty() {
764                                format!("[{prop_name_str}]")
765                            } else {
766                                format!("{path}.{prop_name_str}")
767                            },
768                        )
769                        .map_err(|e| {
770                            ValidationError::PropertyValidationFailed {
771                                property: prop_name_str.clone(),
772                                path: path.into(),
773                                error: Box::new(e),
774                            }
775                        })?;
776                    }
777                }
778            }
779        }
780
781        Ok(())
782    }
783    fn value_type_name(value: &Value) -> String {
784        match value {
785            Value::Null => "null".into(),
786            Value::Bool(_) => "boolean".into(),
787            Value::Number(_) => "number".into(),
788            Value::String(_) => "string".into(),
789            Value::Array(_) => "array".into(),
790            Value::Set(_) => "set".into(),
791            Value::Object(_) => "object".into(),
792            Value::Undefined => "undefined".into(),
793        }
794    }
795}