oapi_codegen/error.rs
1//! Error types for the generator.
2
3/// Errors that can occur while loading a spec or generating code.
4#[derive(Debug)]
5#[non_exhaustive]
6pub enum Error {
7 /// The spec file cannot be read from disk.
8 ReadSpec {
9 /// Path that cannot be read.
10 path: String,
11 /// Underlying IO error.
12 source: std::io::Error,
13 },
14
15 /// The spec file cannot be parsed as OpenAPI YAML/JSON.
16 ParseSpec {
17 /// Path that cannot be parsed.
18 path: String,
19 /// Underlying parse error.
20 source: serde_yaml::Error,
21 },
22
23 /// A referenced external file cannot be read from disk.
24 ReadRefFile {
25 /// The referenced file, as written in the `$ref`.
26 file: String,
27 /// Underlying IO error.
28 source: std::io::Error,
29 },
30
31 /// A referenced external file cannot be parsed as OpenAPI YAML/JSON.
32 ParseRefFile {
33 /// The referenced file, as written in the `$ref`.
34 file: String,
35 /// Underlying parse error.
36 source: serde_yaml::Error,
37 },
38
39 /// The configuration file cannot be read from disk.
40 ReadConfig {
41 /// Path that cannot be read.
42 path: String,
43 /// Underlying IO error.
44 source: std::io::Error,
45 },
46
47 /// The configuration file cannot be parsed as YAML.
48 ParseConfig {
49 /// Path that cannot be parsed.
50 path: String,
51 /// Underlying parse error.
52 source: serde_yaml::Error,
53 },
54
55 /// The requested generation mode is not implemented yet.
56 Unimplemented(String),
57
58 /// Writing the generated output failed.
59 WriteOutput {
60 /// Path that cannot be written.
61 path: String,
62 /// Underlying IO error.
63 source: std::io::Error,
64 },
65
66 /// Reading the output file for a comparison failed.
67 ///
68 /// An absent file is not this error. `--check` reports an absent file as
69 /// drift, because generation creates it. This covers a file that exists and
70 /// that the process cannot read, such as a directory or a file with no read
71 /// permission.
72 ReadOutput {
73 /// Path that cannot be read.
74 path: String,
75 /// Underlying IO error.
76 source: std::io::Error,
77 },
78
79 /// The document declares an OpenAPI version the generator does not read.
80 ///
81 /// Only `3.0.x` is supported. A newer document is rejected and not read as a
82 /// 3.0 document, because the dialects overlap: one whose every construct
83 /// happens to parse as 3.0 would generate quietly, and one newer construct in
84 /// the same file would fail with a `serde` message that names no version.
85 UnsupportedSpecVersion {
86 /// The document that declares it, as a path or as the `$ref` that
87 /// reached it. Every parsed document is checked, so the message must
88 /// name which one failed.
89 document: String,
90 /// The `openapi:` value the document declares.
91 version: String,
92 /// What the generator reads instead.
93 hint: String,
94 },
95
96 /// The document declares a top-level key the generator cannot generate from.
97 ///
98 /// `webhooks:` is the case. It is a 3.1 key that carries operations, and a
99 /// generator that ignores it emits no handler for any of them. Silence here
100 /// reads as "the spec declares no such operation".
101 UnsupportedSpecKey {
102 /// The top-level key, as written in the document.
103 key: String,
104 /// Why the generator cannot generate from it.
105 reason: String,
106 /// What to do instead.
107 hint: String,
108 },
109
110 /// A `$ref` pointed at something that cannot be resolved.
111 UnresolvedRef(String),
112
113 /// A `$ref` used a form the generator does not support yet.
114 UnsupportedRef {
115 /// The offending reference string.
116 reference: String,
117 /// Why it is unsupported.
118 reason: String,
119 },
120
121 /// An `x-` extension carried a value of a kind the generator cannot read.
122 ///
123 /// The author wrote the key to change something. A silent fallback to the
124 /// default would hide that nothing changed.
125 InvalidExtensionValue {
126 /// The extension key, for example `x-rust-name`.
127 key: String,
128 /// The place the key sits, for example a schema or a server URL.
129 at: String,
130 /// The kind of value the key needs.
131 expected: String,
132 /// The kind of value the document gave.
133 found: String,
134 },
135
136 /// A schema combined keywords in a way the generator cannot represent.
137 UnsupportedSchema {
138 /// Dotted path to the schema for diagnostics.
139 path: String,
140 /// Why it is unsupported.
141 reason: String,
142 },
143
144 /// Inline schema nesting exceeded the depth the generator will lower,
145 /// guarding against stack exhaustion on hostile or pathological specs.
146 SchemaDepthExceeded {
147 /// Schema name / lowering hint identifying the offending inline schema.
148 path: String,
149 /// The maximum supported inline nesting depth.
150 limit: usize,
151 },
152
153 /// An operation used a feature the server generator does not support yet.
154 UnsupportedOperation {
155 /// HTTP method of the offending operation.
156 method: String,
157 /// Templated request path of the offending operation.
158 path: String,
159 /// Why it is unsupported.
160 reason: String,
161 },
162
163 /// A request or response body declares content, and no content type the
164 /// generator can represent.
165 ///
166 /// This is not a bodyless body. A bodyless response declares no `content:`
167 /// at all, and `204` is the common case. A body that declares
168 /// `application/pdf` states that a payload exists, so emitting no field for
169 /// it drops the payload with no message.
170 ///
171 /// Both directions report through this one variant, because both make the
172 /// same statement about the same input. The remedy differs by direction, so
173 /// the hint carries it.
174 UnsupportedContentType {
175 /// HTTP method of the offending operation.
176 method: String,
177 /// Templated request path of the offending operation.
178 path: String,
179 /// Which body it is, as a noun phrase for the message (`request body`,
180 /// or a response named by its status code).
181 location: String,
182 /// The declared content types, in document order, comma separated.
183 declared: String,
184 hint: String,
185 },
186
187 /// The schema `default` does not fit the Rust type of the field. Either the
188 /// two disagree, or the value has no literal form here. A dropped default
189 /// leaves the document and the code in disagreement.
190 UnsupportedDefault {
191 /// The type that owns the property.
192 owner: String,
193 /// The property's name as the document writes it.
194 property: String,
195 /// The offending `default`, as JSON.
196 declared: String,
197 hint: String,
198 },
199
200 /// A parameter declared `in: path` has no matching `{placeholder}` in the
201 /// operation's path template. An OpenAPI path parameter must appear in the
202 /// path, and lowering it from the template will otherwise silently drop it
203 /// from the generated signature.
204 InvalidPathParameter {
205 /// HTTP method of the offending operation.
206 method: String,
207 /// Templated request path of the offending operation.
208 path: String,
209 /// The declared path-parameter name with no matching placeholder.
210 name: String,
211 },
212
213 /// A `{placeholder}` in the operation's path template has no matching
214 /// parameter declared `in: path`. The generator cannot know the parameter's
215 /// type, so rather than silently assume `String` it requires the parameter
216 /// to be declared (matching the OpenAPI requirement that every path template
217 /// variable have a corresponding path parameter).
218 UndeclaredPathParameter {
219 /// HTTP method of the offending operation.
220 method: String,
221 /// Templated request path of the offending operation.
222 path: String,
223 /// The template placeholder name with no declared parameter.
224 name: String,
225 },
226
227 /// A generated per-operation type name collided with a component-model name
228 /// emitted in the same file.
229 TypeNameCollision {
230 /// The clashing Rust identifier.
231 name: String,
232 /// The generated artifact that clashed (for example `response enum`).
233 artifact: String,
234 /// How to resolve the clash. Rendered by the console as a hint, and not
235 /// by `Display`, so the console does not print it twice.
236 hint: String,
237 },
238
239 /// A generated type took the name of a Rust prelude type that the emitted
240 /// code writes unqualified, such as `Option` or `Vec`.
241 ///
242 /// The name does not duplicate an emitted item, so no other collision check
243 /// sees it. It shadows the prelude inside the generated file instead, and
244 /// every use of the shadowed type there stops compiling.
245 PreludeShadowing {
246 /// The Rust type name that shadows the prelude.
247 name: String,
248 /// What generated code can name the shadowed type for, for example
249 /// `every optional field`. The check reads names, not uses, so the file
250 /// at hand does not have to hold one.
251 used_for: String,
252 /// How to resolve the clash. Rendered by the console as a hint, and not
253 /// by `Display`, so the console does not print it twice.
254 hint: String,
255 },
256
257 /// Two emitted items took one Rust type name, and at least one of them came
258 /// from an inline schema that lowering hoisted to the crate root.
259 ///
260 /// Two component schemas that collapse onto one identifier are reported as
261 /// [`Error::SchemaNameCollision`], which names both schemas. A hoisted inline
262 /// schema has no name of its own, so this variant names the identifier only.
263 DuplicateTypeName {
264 /// The Rust type name that two emitted items take.
265 name: String,
266 /// How to resolve the clash. Rendered by the console as a hint, and not
267 /// by `Display`, so the console does not print it twice.
268 hint: String,
269 },
270
271 /// A per-operation type took the Rust type name of a second per-operation
272 /// type, or of a generator interface.
273 ///
274 /// Every per-operation type name derives from the method name of its
275 /// operation and a fixed suffix, so two of them clash when a configured suffix
276 /// makes them equal, or when two method names differ only by a suffix that
277 /// another artifact also adds. The same suffix can also give a per-operation
278 /// type the fixed name of a requested interface, such as the `Api` trait.
279 ///
280 /// A clash with a model is reported as [`Error::TypeNameCollision`] instead,
281 /// because the remedy names the schema and not an operation.
282 OperationTypeCollision {
283 /// The Rust type name that both items take.
284 name: String,
285 /// The item that claimed the name first, in document order, as a noun
286 /// phrase. A per-operation type names its kind and its operation. A
287 /// generator interface names what emits it and holds no operation, because
288 /// the name is fixed and belongs to no operation.
289 first: String,
290 /// The item that collided with `first`, always a per-operation type, in the
291 /// same form.
292 second: String,
293 /// How to resolve the clash. Rendered by the console as a hint, and not
294 /// by `Display`, so the console does not print it twice.
295 hint: String,
296 },
297
298 /// Two or more type aliases refer to each other in a cycle.
299 ///
300 /// `type A = B; type B = A;` is a cycle rustc rejects with `E0391`, and no
301 /// amount of indirection fixes it: a `Box` around either side still expands
302 /// forever. The recursion pass boxes a struct field or a union variant, and
303 /// a cycle made only of aliases offers neither.
304 RecursiveAlias {
305 /// The alias names on the cycle, in the order the walk met them.
306 cycle: Vec<String>,
307 /// How to break the cycle. Rendered by the console as a hint, and not by
308 /// `Display`, so the console does not print it twice.
309 hint: String,
310 },
311
312 /// Two component schema names collapsed onto one Rust identifier.
313 ///
314 /// The generator will not choose which schema keeps the plain name, because
315 /// that choice belongs to the spec author.
316 SchemaNameCollision {
317 /// The Rust identifier that both schemas produce.
318 ident: String,
319 /// The schema that claimed the identifier first, in document order.
320 first: String,
321 /// The schema that collided with `first`.
322 second: String,
323 /// How to resolve the clash. Rendered by the console as a hint, and not
324 /// by `Display`, so the console does not print it twice.
325 hint: String,
326 },
327
328 /// Two operations collapsed onto one Rust method name.
329 ///
330 /// Every artifact of an operation derives from this one name, so the file
331 /// holds a duplicate trait method, response enum, and handler, and the router
332 /// points both routes at one handler. The generator will not choose which
333 /// operation keeps the plain name, because that choice belongs to the spec
334 /// author.
335 OperationNameCollision {
336 /// The Rust method name that both operations produce.
337 ident: String,
338 /// `method path` of the operation that claimed the name first, in
339 /// document order.
340 first: String,
341 /// `method path` of the operation that collided with `first`.
342 second: String,
343 /// How to resolve the clash. Rendered by the console as a hint, and not
344 /// by `Display`, so the console does not print it twice.
345 hint: String,
346 },
347
348 /// `output-options.type-name-suffix` holds no identifier characters.
349 ///
350 /// Casing drops punctuation and separators, so a suffix such as `-` or `_`
351 /// adds nothing to a type name. The generator cannot resolve a collision with
352 /// such a suffix, because the second name stays the same as the first.
353 InvalidTypeNameSuffix {
354 /// The configured suffix, as written in the config.
355 suffix: String,
356 /// How to resolve the problem. Rendered by the console as a hint, and not
357 /// by `Display`, so the console does not print it twice.
358 hint: String,
359 },
360
361 /// The generated token stream was not valid Rust (internal bug).
362 InvalidGeneratedCode {
363 /// Underlying syn parse error.
364 source: syn::Error,
365 },
366
367 /// One pass found several independent semantic problems.
368 ///
369 /// This variant holds two or more problems. One problem returns as itself, so
370 /// a caller can match that variant. See
371 /// [`crate::lower::validate::Diagnostics::into_result`]. This variant never
372 /// nests, because the collector holds leaf errors only.
373 Validation {
374 /// The problems, in discovery order.
375 problems: Vec<Error>,
376 },
377}
378
379impl std::fmt::Display for Error {
380 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381 match self {
382 Error::ReadSpec { path, source } => {
383 return write!(f, "failed to read spec file `{path}`: {source}");
384 }
385 Error::ParseSpec { path, source } => {
386 return write!(f, "failed to parse spec file `{path}`: {source}");
387 }
388 Error::ReadRefFile { file, source } => {
389 return write!(f, "failed to read referenced file `{file}`: {source}");
390 }
391 Error::ParseRefFile { file, source } => {
392 return write!(f, "failed to parse referenced file `{file}`: {source}");
393 }
394 Error::ReadConfig { path, source } => {
395 return write!(f, "failed to read config file `{path}`: {source}");
396 }
397 Error::ParseConfig { path, source } => {
398 return write!(f, "failed to parse config file `{path}`: {source}");
399 }
400 Error::Unimplemented(mode) => {
401 return write!(f, "{mode} generation is not implemented yet");
402 }
403 Error::WriteOutput { path, source } => {
404 return write!(f, "failed to write output `{path}`: {source}");
405 }
406 Error::ReadOutput { path, source } => {
407 return write!(f, "failed to read output `{path}`: {source}");
408 }
409 // The remedy is a hint, which the console prints under the message.
410 // `Display` therefore states the problem only.
411 Error::UnsupportedSpecVersion { document, version, .. } => {
412 return write!(f, "`{document}` declares `openapi: {version}`, which is not supported");
413 }
414 Error::UnsupportedSpecKey { key, reason, .. } => {
415 return write!(f, "the document declares `{key}:`, which {reason}");
416 }
417 Error::UnresolvedRef(reference) => {
418 return write!(f, "unresolved reference `{reference}`");
419 }
420 Error::UnsupportedRef { reference, reason } => {
421 return write!(f, "unsupported reference `{reference}`: {reason}");
422 }
423 Error::InvalidExtensionValue {
424 key,
425 at,
426 expected,
427 found,
428 } => {
429 return write!(f, "`{key}` on `{at}` needs {expected}, but the document gives {found}");
430 }
431 Error::UnsupportedSchema { path, reason } => {
432 return write!(f, "unsupported schema at `{path}`: {reason}");
433 }
434 Error::SchemaDepthExceeded { path, limit } => {
435 return write!(f, "schema at `{path}` nests deeper than the supported limit of {limit}");
436 }
437 Error::UnsupportedOperation { method, path, reason } => {
438 return write!(f, "unsupported operation `{method} {path}`: {reason}");
439 }
440 Error::UnsupportedContentType {
441 method,
442 path,
443 location,
444 declared,
445 ..
446 } => {
447 return write!(
448 f,
449 "the {location} of `{method} {path}` declares only content types the generator cannot represent: {declared}"
450 );
451 }
452 Error::UnsupportedDefault {
453 owner,
454 property,
455 declared,
456 ..
457 } => {
458 return write!(
459 f,
460 "the `default` of `{owner}.{property}` cannot be represented as a value of the property's Rust type: {declared}"
461 );
462 }
463 Error::InvalidPathParameter { method, path, name } => {
464 return write!(
465 f,
466 "path parameter `{name}` on `{method} {path}` is declared `in: path` but the path template has no `{{{name}}}` placeholder"
467 );
468 }
469 Error::UndeclaredPathParameter { method, path, name } => {
470 return write!(
471 f,
472 "operation `{method} {path}` has a `{{{name}}}` placeholder in its path but no parameter named `{name}` is declared `in: path`"
473 );
474 }
475 // The remedy is a hint, which the console prints under the message.
476 // `Display` therefore states the problem only.
477 Error::TypeNameCollision { name, artifact, .. } => {
478 return write!(
479 f,
480 "generated {artifact} `{name}` collides with a component schema of the same name"
481 );
482 }
483 Error::PreludeShadowing { name, used_for, .. } => {
484 return write!(
485 f,
486 "generated type `{name}` shadows the Rust prelude type of that name, which generated code can name without a path for {used_for}"
487 );
488 }
489 Error::DuplicateTypeName { name, .. } => {
490 return write!(f, "two generated items both take the Rust type name `{name}`");
491 }
492 Error::OperationTypeCollision {
493 name, first, second, ..
494 } => {
495 return write!(f, "{first} and {second} both take the Rust type name `{name}`");
496 }
497 Error::RecursiveAlias { cycle, .. } => {
498 return write!(f, "type aliases refer to each other in a cycle: {}", cycle.join(" -> "));
499 }
500 Error::SchemaNameCollision {
501 ident, first, second, ..
502 } => {
503 return write!(
504 f,
505 "component schemas `{first}` and `{second}` both produce the Rust type name `{ident}`"
506 );
507 }
508 Error::OperationNameCollision {
509 ident, first, second, ..
510 } => {
511 return write!(
512 f,
513 "operations `{first}` and `{second}` both produce the Rust method name `{ident}`"
514 );
515 }
516 Error::InvalidTypeNameSuffix { suffix, .. } => {
517 return write!(
518 f,
519 "`type-name-suffix` is set to `{suffix}`, which contributes no characters to a Rust type name"
520 );
521 }
522 Error::InvalidGeneratedCode { source } => {
523 return write!(f, "generated code was not valid Rust: {source}");
524 }
525 Error::Validation { problems } => {
526 write!(f, "found {} problems in the spec:", problems.len())?;
527 for problem in problems {
528 write!(f, "\n - {problem}")?;
529 }
530 return Ok(());
531 }
532 }
533 }
534}
535
536impl std::error::Error for Error {
537 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
538 match self {
539 Error::ReadSpec { source, .. } => return Some(source),
540 Error::ParseSpec { source, .. } => return Some(source),
541 Error::ReadRefFile { source, .. } => return Some(source),
542 Error::ParseRefFile { source, .. } => return Some(source),
543 Error::ReadConfig { source, .. } => return Some(source),
544 Error::ParseConfig { source, .. } => return Some(source),
545 Error::WriteOutput { source, .. } => return Some(source),
546 Error::ReadOutput { source, .. } => return Some(source),
547 Error::InvalidGeneratedCode { source } => return Some(source),
548 // `Validation` holds problems at the same level and wraps no cause.
549 // It has no single `source`. `Display` shows the problems instead.
550 Error::Validation { .. }
551 | Error::Unimplemented(_)
552 | Error::UnsupportedSpecVersion { .. }
553 | Error::UnsupportedSpecKey { .. }
554 | Error::UnsupportedContentType { .. }
555 | Error::UnsupportedDefault { .. }
556 | Error::UnresolvedRef(_)
557 | Error::UnsupportedRef { .. }
558 | Error::InvalidExtensionValue { .. }
559 | Error::UnsupportedSchema { .. }
560 | Error::SchemaDepthExceeded { .. }
561 | Error::TypeNameCollision { .. }
562 | Error::DuplicateTypeName { .. }
563 | Error::PreludeShadowing { .. }
564 | Error::OperationTypeCollision { .. }
565 | Error::SchemaNameCollision { .. }
566 | Error::RecursiveAlias { .. }
567 | Error::OperationNameCollision { .. }
568 | Error::InvalidTypeNameSuffix { .. }
569 | Error::InvalidPathParameter { .. }
570 | Error::UndeclaredPathParameter { .. }
571 | Error::UnsupportedOperation { .. } => return None,
572 }
573 }
574}
575
576/// Convenience alias for results in this crate.
577pub type Result<T> = std::result::Result<T, Error>;