satay_codegen/error/validation.rs
1use oas3::spec::ExtensionError;
2use serde::de::Error as SerdeError;
3
4/// Errors that can occur while validating an OpenAPI document.
5///
6/// This enum is [`non_exhaustive`](https://doc.rust-lang.org/reference/attributes/type_system.html)
7/// so new variants may be added in future releases without a semver break.
8#[derive(Debug, thiserror::Error)]
9#[non_exhaustive]
10pub enum ValidationError {
11 // -- Document and component shape validation --
12 /// The OpenAPI version is not supported.
13 ///
14 /// Error message: `unsupported OpenAPI version `{version}`; Satay supports OpenAPI 3.1`
15 #[error("unsupported OpenAPI version `{version}`; Satay supports OpenAPI 3.1")]
16 UnsupportedOpenApiVersion { version: String },
17
18 /// A schema component uses a type that is not supported.
19 ///
20 /// Error message: `unsupported type `{kind}` in schema `{schema}``
21 #[error("unsupported type `{kind}` in schema `{schema}`")]
22 UnsupportedComponentType { schema: String, kind: String },
23
24 /// A schema component is missing a required `type`, `$ref`, `enum`, or `properties` declaration.
25 ///
26 /// Error message: `schema `{schema}` must declare `type`, `$ref`, `enum`, or `properties``
27 #[error("schema `{schema}` must declare `type`, `$ref`, `enum`, or `properties`")]
28 MissingComponentSchemaType { schema: String },
29
30 /// An object schema is missing the required `properties` field.
31 ///
32 /// Error message: `object schema `{schema}` must declare `properties``
33 #[error("object schema `{schema}` must declare `properties`")]
34 MissingObjectProperties { schema: String },
35
36 /// The OpenAPI document is missing the required `paths` field.
37 ///
38 /// Error message: `OpenAPI document must declare `paths``
39 #[error("OpenAPI document must declare `paths`")]
40 MissingPaths,
41
42 // -- Enum and schema type validation --
43 /// A schema uses an enum with a non-string type.
44 ///
45 /// Error message: `{context} uses enum type `{kind}`; only string enums are supported`
46 #[error("{context} uses enum type `{kind}`; only string enums are supported")]
47 UnsupportedEnumType { context: String, kind: String },
48
49 /// A schema declares an enum that is not an array.
50 ///
51 /// Error message: `{context} has a non-array enum`
52 #[error("{context} has a non-array enum")]
53 NonArrayEnum { context: String },
54
55 /// A schema declares an enum with no values.
56 ///
57 /// Error message: `{context} has an empty enum`
58 #[error("{context} has an empty enum")]
59 EmptyEnum { context: String },
60
61 /// A schema enum contains a non-string value.
62 ///
63 /// Error message: `{context} contains a non-string enum value; only string enums are supported`
64 #[error("{context} contains a non-string enum value; only string enums are supported")]
65 NonStringEnumValue { context: String },
66
67 /// A schema declares a `const` value that is not one of its `enum` values.
68 ///
69 /// Error message: `{context} declares a `const` value that is not in its `enum``
70 #[error("{context} declares a `const` value that is not in its `enum`")]
71 ConstNotInEnum { context: String },
72
73 /// A typed specification extension could not be deserialized.
74 ///
75 /// Error message: `{context}: failed to deserialize specification extension at `{path}`: {source}`
76 #[error("{context}: failed to deserialize specification extension at `{path}`: {source}")]
77 InvalidExtension {
78 context: String,
79 path: String,
80 source: serde_json::Error,
81 },
82
83 /// An `x-satay.enum-variants` entry points at a value that is not in the enum.
84 ///
85 /// Error message: `{context}.x-satay.enum-variants contains `{wire_name}`, which is not declared in the enum`
86 #[error(
87 "{context}.x-satay.enum-variants contains `{wire_name}`, which is not declared in the enum"
88 )]
89 UnknownSatayEnumVariantValue { context: String, wire_name: String },
90
91 /// An `x-satay.enum-variants` entry uses a name reserved for generated fallback variants.
92 ///
93 /// Error message: `{context}.x-satay.enum-variants[{wire_name:?}] uses reserved fallback variant `{rust_name}``
94 #[error(
95 "{context}.x-satay.enum-variants[{wire_name:?}] uses reserved fallback variant `{rust_name}`"
96 )]
97 ReservedSatayEnumVariantName {
98 context: String,
99 wire_name: String,
100 rust_name: String,
101 },
102
103 /// Two `x-satay.enum-variants` entries produce the same Rust variant name.
104 ///
105 /// Error message: `{context}.x-satay.enum-variants maps multiple values to `{rust_name}``
106 #[error("{context}.x-satay.enum-variants maps multiple values to `{rust_name}`")]
107 DuplicateSatayEnumVariantName { context: String, rust_name: String },
108
109 /// A schema has a `required` field that is not an array.
110 ///
111 /// Error message: `{context} has a non-array `required` field`
112 #[error("{context} has a non-array `required` field")]
113 NonArrayRequired { context: String },
114
115 /// A schema `required` array contains a non-string element.
116 ///
117 /// Error message: `{context} has a non-string required field name`
118 #[error("{context} has a non-string required field name")]
119 NonStringRequiredField { context: String },
120
121 /// An integer schema uses an unsupported format.
122 ///
123 /// Error message: `{context} uses unsupported integer format `{format}``
124 #[error("{context} uses unsupported integer format `{format}`")]
125 UnsupportedIntegerFormat { context: String, format: String },
126
127 /// A number schema uses an unsupported format.
128 ///
129 /// Error message: `{context} uses unsupported number format `{format}``
130 #[error("{context} uses unsupported number format `{format}`")]
131 UnsupportedNumberFormat { context: String, format: String },
132
133 /// `x-satay.parse-as` was applied to an unsupported wire schema.
134 ///
135 /// Error message: `{context} uses x-satay.parse-as `{parse_as}` on `{kind}`; supported parse-as wire schemas are string schemas, plus integer schemas for bool`
136 #[error(
137 "{context} uses x-satay.parse-as `{parse_as}` on `{kind}`; supported parse-as wire schemas are string schemas, plus integer schemas for bool"
138 )]
139 SatayParseAsRequiresString {
140 context: String,
141 parse_as: String,
142 kind: String,
143 },
144
145 /// An `x-satay.none-if` array is empty.
146 ///
147 /// Error message: `{context}.x-satay.none-if must contain at least one string`
148 #[error("{context}.x-satay.none-if must contain at least one string")]
149 EmptySatayNoneIf { context: String },
150
151 /// `x-satay.none-if` was applied outside a struct property.
152 ///
153 /// Error message: `{context} uses x-satay.none-if outside a struct field`
154 #[error("{context} uses x-satay.none-if outside a struct field")]
155 SatayNoneIfRequiresStructField { context: String },
156
157 /// `x-satay.none-if` was not paired with a string-backed parser.
158 ///
159 /// Error message: `{context} uses x-satay.none-if without a string-backed x-satay.parse-as`
160 #[error("{context} uses x-satay.none-if without a string-backed x-satay.parse-as")]
161 SatayNoneIfRequiresParsedString { context: String },
162
163 /// `x-satay.none-if` and `x-satay.treat-error-as-none` were combined.
164 ///
165 /// Error message: `{context} cannot combine x-satay.none-if with x-satay.treat-error-as-none`
166 #[error("{context} cannot combine x-satay.none-if with x-satay.treat-error-as-none")]
167 ConflictingSatayNoneHandling { context: String },
168
169 /// `x-satay.ignore` was applied outside an object property.
170 ///
171 /// Error message: `{context} uses x-satay.ignore outside an object property`
172 #[error("{context} uses x-satay.ignore outside an object property")]
173 SatayIgnoreRequiresObjectProperty { context: String },
174
175 /// `x-satay.identifier` was applied outside an object property.
176 ///
177 /// Error message: `{context} uses x-satay.identifier outside an object property`
178 #[error("{context} uses x-satay.identifier outside an object property")]
179 SatayIdentifierRequiresObjectProperty { context: String },
180
181 /// An explicit property identifier collides with another Rust field after normalization.
182 ///
183 /// Error message: `{context} maps properties `{first_property}` and `{second_property}` to duplicate Rust field `{rust_name}``
184 #[error(
185 "{context} maps properties `{first_property}` and `{second_property}` to duplicate Rust field `{rust_name}`"
186 )]
187 DuplicateSatayIdentifierRustField {
188 context: String,
189 first_property: String,
190 second_property: String,
191 rust_name: String,
192 },
193
194 /// `x-satay.integer-type` was applied to a non-integer schema.
195 ///
196 /// Error message: `{context} uses x-satay.integer-type `{integer_type}` on `{kind}`; supported integer-type wire schemas are integer schemas and string schemas with x-satay.parse-as integer-range`
197 #[error(
198 "{context} uses x-satay.integer-type `{integer_type}` on `{kind}`; supported integer-type wire schemas are integer schemas and string schemas with x-satay.parse-as integer-range"
199 )]
200 SatayIntegerTypeRequiresInteger {
201 context: String,
202 integer_type: String,
203 kind: String,
204 },
205
206 /// An array schema is missing the required `items` field.
207 ///
208 /// Error message: `{context} array schema must declare `items``
209 #[error("{context} array schema must declare `items`")]
210 MissingArrayItems { context: String },
211
212 /// A schema defines an inline object instead of using a `$ref`.
213 ///
214 /// Error message: `{context} is an inline object schema; move it to components/schemas and use `$ref``
215 #[error("{context} is an inline object schema; move it to components/schemas and use `$ref`")]
216 InlineObjectSchema { context: String },
217
218 /// An object schema has no properties (i.e. acts as a map/dictionary), which is unsupported.
219 ///
220 /// Error message: `{context} is an object with neither `properties` nor a supported `additionalProperties` schema`
221 #[error(
222 "{context} is an object with neither `properties` nor a supported `additionalProperties` schema"
223 )]
224 UnsupportedMapObjectSchema { context: String },
225
226 /// A schema uses an unsupported type.
227 ///
228 /// Error message: `{context} uses unsupported schema type `{kind}``
229 #[error("{context} uses unsupported schema type `{kind}`")]
230 UnsupportedSchemaType { context: String, kind: String },
231
232 /// A schema is missing a required `type`, `$ref`, or `enum` declaration.
233 ///
234 /// Error message: `{context} must declare `type`, `$ref`, or `enum``
235 #[error("{context} must declare `type`, `$ref`, or `enum`")]
236 MissingSchemaType { context: String },
237
238 /// A JSON Schema boolean schema was used; Satay has no IR equivalent yet.
239 ///
240 /// Error message: `{context} is a boolean schema; boolean JSON Schemas are not supported yet`
241 #[error("{context} is a boolean schema; boolean JSON Schemas are not supported yet")]
242 UnsupportedBooleanSchema { context: String },
243
244 /// A schema type array contains more than one non-null type.
245 ///
246 /// Error message: `{context} declares multiple non-null schema types; Satay supports at most one plus null`
247 #[error(
248 "{context} declares multiple non-null schema types; Satay supports at most one plus null"
249 )]
250 MultipleNonNullSchemaTypesUnsupported { context: String },
251
252 /// A schema uses a composition keyword (`allOf`, `anyOf`, `oneOf`) in an unsupported context.
253 ///
254 /// Error message: `{context} uses `{keyword}`, which is not supported in this context`
255 #[error("{context} uses `{keyword}`, which is not supported in this context")]
256 UnsupportedComposition {
257 context: String,
258 keyword: &'static str,
259 },
260
261 /// A schema uses a sibling beside `$ref` that Satay cannot apply.
262 ///
263 /// Error message: `{context} uses unsupported `$ref` sibling `{keyword}``
264 #[error("{context} uses unsupported `$ref` sibling `{keyword}`")]
265 UnsupportedRefSiblingKeyword { context: String, keyword: String },
266
267 /// An `allOf` schema combines supported struct flattening with another schema keyword.
268 ///
269 /// Error message: `{context} uses `allOf` with `{keyword}`; only object branch flattening is supported`
270 #[error("{context} uses `allOf` with `{keyword}`; only object branch flattening is supported")]
271 UnsupportedAllOfSiblingKeyword { context: String, keyword: String },
272
273 /// An `allOf` branch cannot be flattened into a generated Rust struct.
274 ///
275 /// Error message: `{context}.allOf[{index}] must be a local component schema reference or object schema with properties`
276 #[error(
277 "{context}.allOf[{index}] must be a local component schema reference or object schema with properties"
278 )]
279 UnsupportedAllOfBranch { context: String, index: usize },
280
281 /// Two `allOf` branches declare the same object property.
282 ///
283 /// Error message: `{context} declares duplicate `allOf` property `{property}``
284 #[error("{context} declares duplicate `allOf` property `{property}`")]
285 DuplicateAllOfProperty { context: String, property: String },
286
287 /// `allOf` component schemas form a recursive flattening cycle.
288 ///
289 /// Error message: `{context} forms a recursive `allOf` cycle through schema `{schema}``
290 #[error("{context} forms a recursive `allOf` cycle through schema `{schema}`")]
291 RecursiveAllOf { context: String, schema: String },
292
293 /// A discriminator union branch component recursively contains its own union.
294 ///
295 /// Error message: `{context} forms a recursive discriminator cycle through branch schema `{schema}``
296 #[error("{context} forms a recursive discriminator cycle through branch schema `{schema}`")]
297 RecursiveDiscriminatorBranch { context: String, schema: String },
298
299 /// An `anyOf` schema combines a supported union with another schema keyword.
300 ///
301 /// Error message: `{context} uses `anyOf` with `{keyword}`; only annotation siblings are supported`
302 #[error("{context} uses `anyOf` with `{keyword}`; only annotation siblings are supported")]
303 UnsupportedAnyOfSiblingKeyword { context: String, keyword: String },
304
305 /// An `anyOf` branch is not a supported union branch.
306 ///
307 /// Error message: `{context}.anyOf[{index}] must be a local component schema reference, inline string enum, inline primitive schema, or null schema`
308 #[error(
309 "{context}.anyOf[{index}] must be a local component schema reference, inline string enum, inline primitive schema, or null schema"
310 )]
311 UnsupportedAnyOfBranch { context: String, index: usize },
312
313 /// A `oneOf` schema combines a supported union with another schema keyword.
314 ///
315 /// Error message: `{context} uses `oneOf` with `{keyword}`; only annotation siblings are supported`
316 #[error("{context} uses `oneOf` with `{keyword}`; only annotation siblings are supported")]
317 UnsupportedOneOfSiblingKeyword { context: String, keyword: String },
318
319 /// A `oneOf` branch is not a supported union branch.
320 ///
321 /// Error message: `{context}.oneOf[{index}] must be a local component schema reference, inline string enum, inline primitive schema, or null schema`
322 #[error(
323 "{context}.oneOf[{index}] must be a local component schema reference, inline string enum, inline primitive schema, or null schema"
324 )]
325 UnsupportedOneOfBranch { context: String, index: usize },
326
327 /// A plain `anyOf` or `oneOf` union has more than one null branch.
328 ///
329 /// Error message: `{context}.{keyword}[{index}] duplicates the union null branch`
330 #[error("{context}.{keyword}[{index}] duplicates the union null branch")]
331 DuplicateUnionNullBranch {
332 context: String,
333 keyword: &'static str,
334 index: usize,
335 },
336
337 /// An open string enum `anyOf` repeats an enum or `const` value across branches.
338 ///
339 /// Error message: `{context} declares duplicate open string enum value `{value}` across `anyOf` branches`
340 #[error(
341 "{context} declares duplicate open string enum value `{value}` across `anyOf` branches"
342 )]
343 DuplicateOpenStringEnumValue { context: String, value: String },
344
345 /// A nullable plain `anyOf` or `oneOf` union has no non-null branches.
346 ///
347 /// Error message: `{context}.{keyword} must declare at least one non-null branch`
348 #[error("{context}.{keyword} must declare at least one non-null branch")]
349 NullableUnionWithoutVariants {
350 context: String,
351 keyword: &'static str,
352 },
353
354 /// A plain `anyOf` or `oneOf` union has a branch that is statically shadowed by an earlier branch.
355 ///
356 /// Error message: `{context}.{keyword}[{index}] is shadowed by earlier branch {shadowed_by} under ordered serde untagged deserialization`
357 #[error(
358 "{context}.{keyword}[{index}] is shadowed by earlier branch {shadowed_by} under ordered serde untagged deserialization"
359 )]
360 ShadowedUnionBranch {
361 context: String,
362 keyword: &'static str,
363 index: usize,
364 shadowed_by: usize,
365 },
366
367 /// A composition schema declares no branches.
368 ///
369 /// Raised for empty `anyOf` and, today, for empty component `allOf` that is routed
370 /// through the shared empty composition-shape check.
371 ///
372 /// Error message: `{context} must declare at least one `anyOf` branch`
373 #[error("{context} must declare at least one `anyOf` branch")]
374 EmptyAnyOf { context: String },
375
376 /// `anyOf` component schemas form a recursive union cycle.
377 ///
378 /// Error message: `{context} forms a recursive `anyOf` cycle through schema `{schema}``
379 #[error("{context} forms a recursive `anyOf` cycle through schema `{schema}`")]
380 RecursiveAnyOf { context: String, schema: String },
381
382 /// A discriminator union does not use exactly one non-empty `anyOf` or `oneOf` branch list.
383 ///
384 /// Error message: `{context} discriminator unions must declare exactly one non-empty `anyOf` or `oneOf` branch list`
385 #[error(
386 "{context} discriminator unions must declare exactly one non-empty `anyOf` or `oneOf` branch list"
387 )]
388 InvalidDiscriminatorUnion { context: String },
389
390 /// A discriminator union branch is not a local component schema reference.
391 ///
392 /// Error message: `{context}.{keyword}[{index}] must be a local component schema reference when using `discriminator``
393 #[error(
394 "{context}.{keyword}[{index}] must be a local component schema reference when using `discriminator`"
395 )]
396 UnsupportedDiscriminatorBranch {
397 context: String,
398 keyword: &'static str,
399 index: usize,
400 },
401
402 /// A discriminator union branch target does not generate as an object struct.
403 ///
404 /// Error message: `{context} discriminator branch `{schema}` must be an object struct component`
405 #[error("{context} discriminator branch `{schema}` must be an object struct component")]
406 DiscriminatorBranchNotObject { context: String, schema: String },
407
408 /// A discriminator branch object contains the discriminator property.
409 ///
410 /// Error message: `{context} discriminator branch `{schema}` contains discriminator property `{property}``
411 #[error(
412 "{context} discriminator branch `{schema}` contains discriminator property `{property}`"
413 )]
414 DiscriminatorPropertyConflict {
415 context: String,
416 schema: String,
417 property: String,
418 },
419
420 /// A discriminator branch object contains an invalid embedded discriminator property.
421 ///
422 /// Error message: `{context} discriminator branch `{schema}` property `{property}` must be {expected}`
423 #[error("{context} discriminator branch `{schema}` property `{property}` must be {expected}")]
424 InvalidDiscriminatorProperty {
425 context: String,
426 schema: String,
427 property: String,
428 expected: &'static str,
429 },
430
431 /// A discriminator mapping entry targets a non-local schema or a schema outside the union branches.
432 ///
433 /// Error message: `{context}.discriminator.mapping[{value:?}] targets `{target}`, which is not a local union branch schema`
434 #[error(
435 "{context}.discriminator.mapping[{value:?}] targets `{target}`, which is not a local union branch schema"
436 )]
437 InvalidDiscriminatorMapping {
438 context: String,
439 value: String,
440 target: String,
441 },
442
443 /// Multiple discriminator mapping values target the same union branch schema.
444 ///
445 /// Error message: `{context}.discriminator.mapping maps multiple values to branch schema `{schema}``
446 #[error("{context}.discriminator.mapping maps multiple values to branch schema `{schema}`")]
447 DuplicateDiscriminatorMapping { context: String, schema: String },
448
449 /// A discriminator mapping value disagrees with a branch's embedded discriminator property value.
450 ///
451 /// Error message: `{context}.discriminator.mapping maps value `{value}` to branch schema `{schema}`, but the branch declares discriminator value `{actual}`
452 #[error(
453 "{context}.discriminator.mapping maps value `{value}` to branch schema `{schema}`, but the branch declares discriminator value `{actual}`"
454 )]
455 DiscriminatorMappingValueMismatch {
456 context: String,
457 schema: String,
458 value: String,
459 actual: String,
460 },
461
462 /// Multiple discriminator branches resolve to the same discriminator value after implicit defaults are applied.
463 ///
464 /// Error message: `{context}.discriminator resolves multiple branch schemas to value `{value}``
465 #[error("{context}.discriminator resolves multiple branch schemas to value `{value}`")]
466 DuplicateDiscriminatorValue { context: String, value: String },
467
468 // -- Schema constraint validation --
469 /// A string schema specifies a `minLength` greater than its `maxLength`.
470 ///
471 /// Error message: `{context} has minLength {min_length} greater than maxLength {max_length}`
472 #[error("{context} has minLength {min_length} greater than maxLength {max_length}")]
473 InvalidStringLengthBounds {
474 context: String,
475 min_length: u64,
476 max_length: u64,
477 },
478
479 /// A schema uses `uniqueItems`, which cannot be enforced by generated `Vec`-backed types.
480 ///
481 /// Error message: `{context} uses `uniqueItems`; generated Vec-backed types cannot enforce uniqueness yet`
482 #[error(
483 "{context} uses `uniqueItems`; generated Vec-backed types cannot enforce uniqueness yet"
484 )]
485 UniqueItemsUnsupported { context: String },
486
487 /// An array schema specifies `minItems` greater than `maxItems`.
488 ///
489 /// Error message: `{context} has minItems {min_items} greater than maxItems {max_items}`
490 #[error("{context} has minItems {min_items} greater than maxItems {max_items}")]
491 InvalidArrayLengthBounds {
492 context: String,
493 min_items: u64,
494 max_items: u64,
495 },
496
497 /// A schema uses a keyword that is not safely supported.
498 ///
499 /// Error message: `{context} uses `{keyword}`, which is not safely supported yet`
500 #[error("{context} uses `{keyword}`, which is not safely supported yet")]
501 UnsupportedKeyword { context: String, keyword: String },
502
503 /// A schema keyword that must be a non-negative integer has an invalid value.
504 ///
505 /// Error message: `{context}.{keyword} must be a non-negative integer`
506 #[error("{context}.{keyword} must be a non-negative integer")]
507 InvalidNonNegativeIntegerKeyword {
508 context: String,
509 keyword: &'static str,
510 },
511
512 /// An `exclusiveMinimum`/`exclusiveMaximum` keyword is present but the corresponding bound is missing.
513 ///
514 /// Error message: `{context}.{exclusive_keyword} requires `{keyword}``
515 #[error("{context}.{exclusive_keyword} requires `{keyword}`")]
516 ExclusiveLimitRequiresBound {
517 context: String,
518 exclusive_keyword: &'static str,
519 keyword: &'static str,
520 },
521
522 /// A schema keyword that must be a finite number has a non-finite value.
523 ///
524 /// Error message: `{context}.{keyword} must be a finite number`
525 #[error("{context}.{keyword} must be a finite number")]
526 InvalidFiniteNumberKeyword {
527 context: String,
528 keyword: &'static str,
529 },
530
531 /// A value expected to be an integer is not.
532 ///
533 /// Error message: `{context} must be an integer`
534 #[error("{context} must be an integer")]
535 ExpectedInteger { context: String },
536
537 /// Integer bounds (minimum/maximum) do not permit any value.
538 ///
539 /// Error message: `{context} integer bounds do not allow any value`
540 #[error("{context} integer bounds do not allow any value")]
541 EmptyIntegerBounds { context: String },
542
543 /// An exclusive integer minimum overflows `i64`.
544 ///
545 /// Error message: `exclusive integer minimum overflows`
546 #[error("exclusive integer minimum overflows")]
547 ExclusiveIntegerMinimumOverflow,
548
549 /// An exclusive integer maximum overflows `i64`.
550 ///
551 /// Error message: `exclusive integer maximum overflows`
552 #[error("exclusive integer maximum overflows")]
553 ExclusiveIntegerMaximumOverflow,
554
555 /// Number bounds (minimum/maximum) do not permit any value.
556 ///
557 /// Error message: `{context} number bounds do not allow any value`
558 #[error("{context} number bounds do not allow any value")]
559 EmptyNumberBounds { context: String },
560
561 // -- Operation, parameter, and response validation --
562 /// An operation does not declare any responses.
563 ///
564 /// Error message: `operation `{operation_id}` must declare responses`
565 #[error("operation `{operation_id}` must declare responses")]
566 MissingOperationResponses { operation_id: String },
567
568 /// A value expected to be an array is not.
569 ///
570 /// Error message: `{context} must be an array`
571 #[error("{context} must be an array")]
572 ExpectedArray { context: String },
573
574 /// A parameter uses an unsupported location (e.g. cookie) instead of path, query, or header.
575 ///
576 /// Error message: `{context} parameter `{wire_name}` is in `{location}`; only path, query, and header parameters are supported`
577 #[error(
578 "{context} parameter `{wire_name}` is in `{location}`; only path, query, and header parameters are supported"
579 )]
580 UnsupportedParameterLocation {
581 context: String,
582 wire_name: String,
583 location: String,
584 },
585
586 /// A parameter uses `content` instead of `schema`.
587 ///
588 /// Error message: `{context} parameter `{wire_name}` uses `content`; schema parameters are required`
589 #[error("{context} parameter `{wire_name}` uses `content`; schema parameters are required")]
590 ContentParameterUnsupported { context: String, wire_name: String },
591
592 /// A parameter is missing a required `schema` declaration.
593 ///
594 /// Error message: `{context} parameter `{wire_name}` must declare schema`
595 #[error("{context} parameter `{wire_name}` must declare schema")]
596 MissingParameterSchema { context: String, wire_name: String },
597
598 /// A parameter is nullable, which is not supported.
599 ///
600 /// Error message: `parameter `{wire_name}` is nullable; nullable parameters are not supported`
601 #[error("parameter `{wire_name}` is nullable; nullable parameters are not supported")]
602 NullableParameterUnsupported { wire_name: String },
603
604 /// A parameter uses `anyOf`, which is not supported for URI/header encoding yet.
605 ///
606 /// Error message: `parameter `{wire_name}` uses `anyOf`; anyOf parameters are not supported yet`
607 #[error("parameter `{wire_name}` uses `anyOf`; anyOf parameters are not supported yet")]
608 AnyOfParameterUnsupported { wire_name: String },
609
610 /// A parameter is a map or arbitrary JSON value, which has no URI/header encoding.
611 ///
612 /// Error message: `parameter `{wire_name}` is a map or JSON value; map parameters are not supported`
613 #[error("parameter `{wire_name}` is a map or JSON value; map parameters are not supported")]
614 MapParameterUnsupported { wire_name: String },
615
616 /// A path parameter is an array, which is not supported.
617 ///
618 /// Error message: `path parameter `{wire_name}` is an array; array path parameter styles are not supported`
619 #[error(
620 "path parameter `{wire_name}` is an array; array path parameter styles are not supported"
621 )]
622 ArrayPathParameterUnsupported { wire_name: String },
623
624 /// A header parameter is an array, which is not supported.
625 ///
626 /// Error message: `header parameter `{wire_name}` is an array; array header parameter styles are not supported`
627 #[error(
628 "header parameter `{wire_name}` is an array; array header parameter styles are not supported"
629 )]
630 ArrayHeaderParameterUnsupported { wire_name: String },
631
632 /// A path parameter does not set `required: true`.
633 ///
634 /// Error message: `path parameter `{wire_name}` must set required: true`
635 #[error("path parameter `{wire_name}` must set required: true")]
636 PathParameterNotRequired { wire_name: String },
637
638 /// A context is missing a required `content` declaration.
639 ///
640 /// Error message: `{context} must declare content`
641 #[error("{context} must declare content")]
642 MissingContent { context: String },
643
644 /// A context is missing the required `application/json` content type.
645 ///
646 /// Error message: `{context} must declare application/json content`
647 #[error("{context} must declare application/json content")]
648 MissingJsonContent { context: String },
649
650 /// A context's `application/json` content is missing a schema.
651 ///
652 /// Error message: `{context} application/json content must declare schema`
653 #[error("{context} application/json content must declare schema")]
654 MissingJsonSchema { context: String },
655
656 /// A response body uses the `default` status, which is not yet supported for decoding.
657 ///
658 /// Error message: `{context} contains a default response body; default response decoding is not supported yet`
659 #[error(
660 "{context} contains a default response body; default response decoding is not supported yet"
661 )]
662 DefaultResponseBodyUnsupported { context: String },
663
664 /// A response contains an invalid HTTP status code string.
665 ///
666 /// Error message: `{context} contains invalid status code `{status}``
667 #[error("{context} contains invalid status code `{status}`")]
668 InvalidStatusCode { context: String, status: String },
669
670 /// A response contains a status code outside the valid 100–599 range.
671 ///
672 /// Error message: `{context} contains out-of-range status code `{status_code}``
673 #[error("{context} contains out-of-range status code `{status_code}`")]
674 OutOfRangeStatusCode { context: String, status_code: u16 },
675
676 /// A response for a given status code is missing `application/json` content.
677 ///
678 /// Error message: `{context} {status} response must declare application/json content`
679 #[error("{context} {status} response must declare application/json content")]
680 MissingResponseJsonContent { context: String, status: String },
681
682 /// `x-satay.output` was configured on an operation with no JSON response body.
683 ///
684 /// Error message: `operation `{operation_id}` uses x-satay.output but has no JSON response body`
685 #[error("operation `{operation_id}` uses x-satay.output but has no JSON response body")]
686 SatayOutputRequiresResponseBody { operation_id: String },
687
688 /// A response projection selector was applied to a schema that is not an object with fields.
689 ///
690 /// Error message: `{context} cannot apply x-satay.output.{selector}; expected an object schema with properties`
691 #[error(
692 "{context} cannot apply x-satay.output.{selector}; expected an object schema with properties"
693 )]
694 SatayOutputExpectedObject {
695 context: String,
696 selector: &'static str,
697 },
698
699 /// A response projection selector names a field absent from its object schema.
700 ///
701 /// Error message: `{context} x-satay.output.{selector} `{field}` does not match a declared property`
702 #[error("{context} x-satay.output.{selector} `{field}` does not match a declared property")]
703 UnknownSatayOutputField {
704 context: String,
705 selector: &'static str,
706 field: String,
707 },
708
709 /// `x-satay.output.map-field` follows an unwrapped value that is not an array.
710 ///
711 /// Error message: `{context} cannot apply x-satay.output.map-field after `{field}`; the unwrapped schema must be an array`
712 #[error(
713 "{context} cannot apply x-satay.output.map-field after `{field}`; the unwrapped schema must be an array"
714 )]
715 SatayOutputMapRequiresArray { context: String, field: String },
716
717 /// A path template contains a parameter that is never closed.
718 ///
719 /// Error message: `path `{path}` contains an unclosed parameter`
720 #[error("path `{path}` contains an unclosed parameter")]
721 UnclosedPathParameter { path: String },
722
723 /// A path template contains an empty parameter (e.g. `{}`).
724 ///
725 /// Error message: `path `{path}` contains an empty parameter`
726 #[error("path `{path}` contains an empty parameter")]
727 EmptyPathParameter { path: String },
728
729 /// A path template references a parameter that is not declared in the operation's parameters.
730 ///
731 /// Error message: `path `{path}` uses parameter `{name}` but it is not declared`
732 #[error("path `{path}` uses parameter `{name}` but it is not declared")]
733 UndeclaredPathParameter { path: String, name: String },
734
735 /// A parameter is declared for a path but never used in the path template.
736 ///
737 /// Error message: `path parameter `{name}` is declared but not used in path `{path}``
738 #[error("path parameter `{name}` is declared but not used in path `{path}`")]
739 UnusedPathParameter { path: String, name: String },
740
741 // -- Reference resolution and JSON shape validation --
742 /// A `$ref` could not be resolved because the referenced component failed validation.
743 ///
744 /// Error message: `failed to resolve reference `{reference}` in {context}: {source}`
745 #[error("failed to resolve reference `{reference}` in {context}: {source}")]
746 ResolveReference {
747 reference: String,
748 context: String,
749 #[source]
750 source: Box<ValidationError>,
751 },
752
753 /// A reference points to an external document; only local (`#`) references are supported.
754 ///
755 /// Error message: `only local references are supported`
756 #[error("only local references are supported")]
757 NonLocalReference,
758
759 /// A local reference is not a valid JSON pointer.
760 ///
761 /// Error message: `local reference must be a JSON pointer`
762 #[error("local reference must be a JSON pointer")]
763 InvalidLocalReference,
764
765 /// A JSON pointer is missing a required token segment.
766 ///
767 /// Error message: `missing `{token}``
768 #[error("missing `{token}`")]
769 MissingJsonPointerToken { token: String },
770
771 /// A `$ref` does not point to the expected `#/components/{section}/…` path.
772 ///
773 /// Error message: `reference `{reference}` must point to #/components/{section}/...`
774 #[error("reference `{reference}` must point to #/components/{section}/...")]
775 InvalidComponentReference {
776 reference: String,
777 section: &'static str,
778 },
779
780 /// A local `$ref` chain references itself.
781 ///
782 /// Error message: `circular reference `{reference}``
783 #[error("circular reference `{reference}`")]
784 CircularReference { reference: String },
785
786 /// A value expected to be an object is not.
787 ///
788 /// Error message: `{context} must be an object`
789 #[error("{context} must be an object")]
790 ExpectedObject { context: String },
791}
792
793impl ValidationError {
794 /// Wraps an [`ExtensionError`] with a codegen context string such as a schema
795 /// or operation identifier.
796 pub(crate) fn extension_error(context: &str, source: ExtensionError) -> Self {
797 match source {
798 ExtensionError::InvalidValue { path, source, .. } => {
799 ValidationError::InvalidExtension {
800 context: context.to_owned(),
801 path,
802 source,
803 }
804 }
805 // `x-satay` is always a valid extension name when used by codegen.
806 // The invalid-name case only fires when a caller passes a bad name
807 // itself; preserve it here as a fallback diagnostic.
808 other => ValidationError::InvalidExtension {
809 context: context.to_owned(),
810 path: name_or_other_path(other),
811 source: SerdeError::custom("invalid specification extension"),
812 },
813 }
814 }
815}
816
817fn name_or_other_path(error: oas3::spec::ExtensionError) -> String {
818 match error {
819 ExtensionError::InvalidName { name } => name,
820 _ => "x-satay".to_owned(),
821 }
822}