Skip to main content

satay_codegen/error/
validation.rs

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