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