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}