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