oapi_codegen/config.rs
1//! Generator configuration, compatible with `oapi-codegen`'s YAML configuration files.
2
3use std::collections::BTreeMap;
4use std::path::Path;
5use std::path::PathBuf;
6
7use serde::Deserialize;
8
9use crate::error::Error;
10use crate::error::Result;
11
12/// A generator configuration, mirroring the keys used by `oapi-codegen`.
13///
14/// Unknown keys are ignored so that existing `oapi-codegen` configurations can be used
15/// as-is. Only the subset relevant to this tool is interpreted.
16#[derive(Debug, Default, Clone, Deserialize)]
17#[serde(rename_all = "kebab-case")]
18pub struct Config {
19 /// Target module/package name (informational for the Rust generator).
20 pub package: Option<String>,
21 /// Output file path for generated code, when configured.
22 pub output: Option<PathBuf>,
23 /// Which artifacts to generate.
24 #[serde(default)]
25 pub generate: Generate,
26 /// Output tuning options.
27 #[serde(default)]
28 pub output_options: OutputOptions,
29 /// Mapping of referenced spec files to external modules (server generation).
30 #[serde(default)]
31 pub import_mapping: BTreeMap<String, String>,
32}
33
34/// The set of artifacts a configuration requests.
35#[derive(Debug, Default, Clone, Deserialize)]
36#[serde(rename_all = "kebab-case")]
37pub struct Generate {
38 /// Generate data models (structs/enums) from component schemas.
39 #[serde(default)]
40 pub models: bool,
41 /// Generate an axum server interface from the spec's paths.
42 #[serde(default)]
43 pub std_http_server: bool,
44 /// Generate a blocking `reqwest` client from the spec's paths.
45 #[serde(default)]
46 pub client: bool,
47 /// Embed the spec into the generated code (not yet implemented).
48 #[serde(default)]
49 pub embedded_spec: bool,
50 /// Emit constants and builder functions for the spec's `servers` URLs.
51 #[serde(default)]
52 pub server_urls: bool,
53}
54
55/// Config key of the [`Config::output_options`] section, as written in a configuration
56/// file. Must match the `kebab-case` serde name. Guarded by a deserialization
57/// test.
58pub(crate) const OUTPUT_OPTIONS_KEY: &str = "output-options";
59
60/// Config key of [`OutputOptions::response_type_suffix`], as written in a configuration
61/// file. Must match the `kebab-case` serde name. Guarded by a deserialization
62/// test.
63pub(crate) const RESPONSE_TYPE_SUFFIX_KEY: &str = "response-type-suffix";
64
65/// Seed for a response enum's name suffix when
66/// [`OutputOptions::response_type_suffix`] is unset.
67/// The `to_ident(_, Pascal)` call turns it into the `Response` that terminates every
68/// default `<Op>Response` enum.
69pub(crate) const DEFAULT_RESPONSE_SUFFIX: &str = "response";
70
71/// Config key of [`OutputOptions::type_name_suffix`], as written in a config
72/// file. Must match the `kebab-case` serde name; guarded by a deserialization
73/// test.
74pub(crate) const TYPE_NAME_SUFFIX_KEY: &str = "type-name-suffix";
75
76/// Output tuning options.
77#[derive(Debug, Default, Clone, Deserialize)]
78#[serde(rename_all = "kebab-case")]
79pub struct OutputOptions {
80 /// Keep schemas that are not referenced (no pruning).
81 #[serde(default)]
82 pub skip_prune: bool,
83 /// Only generate operations tagged with one of these tags (empty = all).
84 #[serde(default)]
85 pub include_tags: Vec<String>,
86 /// Skip operations tagged with any of these tags.
87 #[serde(default)]
88 pub exclude_tags: Vec<String>,
89 /// Only generate operations whose `operationId` is one of these
90 /// (empty = all).
91 #[serde(default)]
92 pub include_operation_ids: Vec<String>,
93 /// Skip operations whose `operationId` is one of these.
94 #[serde(default)]
95 pub exclude_operation_ids: Vec<String>,
96 /// Remove these component schemas from the spec before lowering, so their
97 /// models are not generated. Filtering runs before pruning. If an excluded
98 /// schema is still referenced by a retained operation or schema, generation
99 /// can fail or emit a reference to a type that is not declared.
100 #[serde(default)]
101 pub exclude_schemas: Vec<String>,
102 /// Suffix appended to a per-operation response enum's name (default
103 /// `Response`). Set this to resolve a clash between a generated
104 /// `<Op>Response` enum and a component schema of the same name, mirroring
105 /// `oapi-codegen`'s `response-type-suffix`.
106 #[serde(default)]
107 pub response_type_suffix: Option<String>,
108 /// Suffix added to the second of two schema names that collapse onto one
109 /// Rust identifier. For example, `foo-bar` and `fooBar` both become `FooBar`.
110 ///
111 /// The default is unset. An unset suffix makes such a collision an error,
112 /// because the generator will not pick a name for one of two distinct
113 /// schemas. Use `x-rust-name` on the colliding schema first. That extension
114 /// marks one schema and records the name the author wants. This option is
115 /// for specs with many mechanical collisions, where one annotation per
116 /// schema costs too much.
117 ///
118 /// A set suffix must hold at least one letter or digit. Casing removes
119 /// punctuation, so a suffix such as `-` leaves the type name unchanged and
120 /// cannot resolve a collision. See [`crate::lower::type_renames`].
121 #[serde(default)]
122 pub type_name_suffix: Option<String>,
123}
124
125impl Config {
126 /// Load and parse a configuration file.
127 pub fn load(path: &Path) -> Result<Self> {
128 let text = std::fs::read_to_string(path).map_err(|source| {
129 return Error::ReadConfig {
130 path: path.display().to_string(),
131 source,
132 };
133 })?;
134 let config: Config = serde_yaml::from_str(&text).map_err(|source| {
135 return Error::ParseConfig {
136 path: path.display().to_string(),
137 source,
138 };
139 })?;
140 return Ok(config);
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn config_keys_match_serde_names() {
150 let yaml =
151 format!("{OUTPUT_OPTIONS_KEY}:\n {RESPONSE_TYPE_SUFFIX_KEY}: Resp\n {TYPE_NAME_SUFFIX_KEY}: Alt\n",);
152 let config: Config = serde_yaml::from_str(&yaml).expect("config parses");
153 assert_eq!(
154 config.output_options.response_type_suffix.as_deref(),
155 Some("Resp"),
156 "OUTPUT_OPTIONS_KEY/RESPONSE_TYPE_SUFFIX_KEY drifted from the serde field names",
157 );
158 assert_eq!(
159 config.output_options.type_name_suffix.as_deref(),
160 Some("Alt"),
161 "TYPE_NAME_SUFFIX_KEY drifted from the serde field name",
162 );
163 }
164
165 #[test]
166 fn type_name_suffix_defaults_to_unset() {
167 // An unset suffix must mean "stop on a collision", so the default is
168 // `None` and not a fallback string.
169 let config: Config = serde_yaml::from_str("package: demo\n").expect("config parses");
170 assert_eq!(config.output_options.type_name_suffix, None);
171 }
172
173 #[test]
174 fn default_response_suffix_pascalizes_to_response() {
175 use crate::naming::Case;
176 use crate::naming::to_ident;
177
178 assert_eq!(to_ident(DEFAULT_RESPONSE_SUFFIX, Case::Pascal).logical(), "Response");
179 }
180}