Skip to main content

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}