Skip to main content

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}