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