Skip to main content

regorus/schema/
error.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4#![allow(clippy::use_debug, clippy::pattern_type_mismatch)]
5
6use crate::*;
7
8type String = Rc<str>;
9
10/// Validation errors that can occur when validating a Value against a Schema.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum ValidationError {
13    /// Value type does not match the expected schema type.
14    TypeMismatch {
15        expected: String,
16        actual: String,
17        path: String,
18    },
19    /// Numeric value is outside the allowed range.
20    OutOfRange {
21        value: String,
22        min: Option<String>,
23        max: Option<String>,
24        path: String,
25    },
26    /// String length constraint violation.
27    LengthConstraint {
28        actual_length: usize,
29        min_length: Option<usize>,
30        max_length: Option<usize>,
31        path: String,
32    },
33    /// String does not match required pattern.
34    PatternMismatch {
35        value: String,
36        pattern: String,
37        path: String,
38    },
39    /// Array size constraint violation.
40    ArraySizeConstraint {
41        actual_size: usize,
42        min_items: Option<usize>,
43        max_items: Option<usize>,
44        path: String,
45    },
46    /// Required object property is missing.
47    MissingRequiredProperty { property: String, path: String },
48    /// Object property failed validation.
49    PropertyValidationFailed {
50        property: String,
51        path: String,
52        error: Box<ValidationError>,
53    },
54    /// Additional properties are not allowed.
55    AdditionalPropertiesNotAllowed { property: String, path: String },
56    /// Value is not in the allowed enum values.
57    NotInEnum {
58        value: String,
59        allowed_values: Vec<String>,
60        path: String,
61    },
62    /// Value does not match the required constant.
63    ConstMismatch {
64        expected: String,
65        actual: String,
66        path: String,
67    },
68    /// Value does not match any schema in a union (anyOf).
69    NoUnionMatch {
70        path: String,
71        errors: Vec<ValidationError>,
72    },
73    /// Invalid regex pattern in schema.
74    InvalidPattern { pattern: String, error: String },
75    /// Array item validation failed.
76    ArrayItemValidationFailed {
77        index: usize,
78        path: String,
79        error: Box<ValidationError>,
80    },
81    /// Object key is not a string.
82    NonStringKey { key_type: String, path: String },
83    /// Missing discriminator field in discriminated subobject.
84    MissingDiscriminator { discriminator: String, path: String },
85    /// Unknown discriminator value in discriminated subobject.
86    UnknownDiscriminatorValue {
87        discriminator: String,
88        value: String,
89        allowed_values: Vec<String>,
90        path: String,
91    },
92    /// Discriminated subobject validation failed.
93    DiscriminatedSubobjectValidationFailed {
94        discriminator: String,
95        value: String,
96        path: String,
97        error: Box<ValidationError>,
98    },
99}
100
101impl fmt::Display for ValidationError {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            ValidationError::TypeMismatch {
105                expected,
106                actual,
107                path,
108            } => {
109                write!(
110                    f,
111                    "Type mismatch at '{path}': expected {expected}, got {actual}"
112                )
113            }
114            ValidationError::OutOfRange {
115                value,
116                min,
117                max,
118                path,
119            } => {
120                let range_desc = match (min, max) {
121                    (Some(min), Some(max)) => format!("between {min} and {max}"),
122                    (Some(min), None) => format!("at least {min}"),
123                    (None, Some(max)) => format!("at most {max}"),
124                    (None, None) => "within valid range".to_string(),
125                };
126                write!(
127                    f,
128                    "Value {value} at '{path}' is out of range: must be {range_desc}"
129                )
130            }
131            ValidationError::LengthConstraint {
132                actual_length,
133                min_length,
134                max_length,
135                path,
136            } => {
137                let constraint_desc = match (min_length, max_length) {
138                    (Some(min), Some(max)) => format!("between {min} and {max} characters"),
139                    (Some(min), None) => format!("at least {min} characters"),
140                    (None, Some(max)) => format!("at most {max} characters"),
141                    (None, None) => "within valid length".to_string(),
142                };
143                write!(
144                    f,
145                    "String length {actual_length} at '{path}' violates constraint: must be {constraint_desc}"
146                )
147            }
148            ValidationError::PatternMismatch {
149                value,
150                pattern,
151                path,
152            } => {
153                write!(
154                    f,
155                    "String '{value}' at '{path}' does not match pattern '{pattern}'"
156                )
157            }
158            ValidationError::ArraySizeConstraint {
159                actual_size,
160                min_items,
161                max_items,
162                path,
163            } => {
164                let constraint_desc = match (min_items, max_items) {
165                    (Some(min), Some(max)) => format!("between {min} and {max} items"),
166                    (Some(min), None) => format!("at least {min} items"),
167                    (None, Some(max)) => format!("at most {max} items"),
168                    (None, None) => "within valid size".to_string(),
169                };
170                write!(
171                    f,
172                    "Array size {actual_size} at '{path}' violates constraint: must have {constraint_desc}"
173                )
174            }
175            ValidationError::MissingRequiredProperty { property, path } => {
176                write!(f, "Missing required property '{property}' at '{path}'")
177            }
178            ValidationError::PropertyValidationFailed {
179                property,
180                path,
181                error,
182            } => {
183                write!(
184                    f,
185                    "Property '{property}' at '{path}' failed validation: {error}"
186                )
187            }
188            ValidationError::AdditionalPropertiesNotAllowed { property, path } => {
189                write!(
190                    f,
191                    "Additional property '{property}' not allowed at '{path}'"
192                )
193            }
194            ValidationError::NotInEnum {
195                value,
196                allowed_values,
197                path,
198            } => {
199                let values_json = serde_json::to_string(&allowed_values)
200                    .unwrap_or_else(|_| format!("{allowed_values:?}"));
201
202                write!(
203                    f,
204                    "Value '{value}' at '{path}' is not in allowed enum values: {values_json}",
205                )
206            }
207            ValidationError::ConstMismatch {
208                expected,
209                actual,
210                path,
211            } => {
212                write!(
213                    f,
214                    "Constant mismatch at '{path}': expected '{expected}', got '{actual}'"
215                )
216            }
217            ValidationError::NoUnionMatch { path, errors } => {
218                write!(
219                    f,
220                    "Value at '{path}' does not match any schema in union. Errors: {errors:?}"
221                )
222            }
223            ValidationError::InvalidPattern { pattern, error } => {
224                write!(f, "Invalid regex pattern '{pattern}': {error}")
225            }
226            ValidationError::ArrayItemValidationFailed { index, path, error } => {
227                write!(
228                    f,
229                    "Array item {index} at '{path}' failed validation: {error}"
230                )
231            }
232            ValidationError::NonStringKey { key_type, path } => {
233                write!(
234                    f,
235                    "Object key at '{path}' must be a string, but found {key_type}"
236                )
237            }
238            ValidationError::MissingDiscriminator {
239                discriminator,
240                path,
241            } => {
242                write!(
243                    f,
244                    "Missing discriminator field '{discriminator}' at '{path}'"
245                )
246            }
247            ValidationError::UnknownDiscriminatorValue {
248                discriminator,
249                value,
250                allowed_values,
251                path,
252            } => {
253                let values_json: Vec<serde_json::Value> = allowed_values
254                    .iter()
255                    .map(|v| serde_json::Value::String(v.to_string()))
256                    .collect();
257                write!(
258                    f,
259                    "Unknown discriminator value '{value}' for field '{discriminator}' at '{path}'. Allowed values: {}",
260                    serde_json::to_string(&values_json).unwrap_or_else(|_| format!("{values_json:?}"))
261                )
262            }
263            ValidationError::DiscriminatedSubobjectValidationFailed {
264                discriminator,
265                value,
266                path,
267                error,
268            } => {
269                write!(
270                    f,
271                    "Discriminated subobject validation failed for discriminator '{discriminator}' with value '{value}' at '{path}': {error}"
272                )
273            }
274        }
275    }
276}
277
278impl core::error::Error for ValidationError {}