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