Skip to main content

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>;