1use std::collections::BTreeMap;
4use std::path::Path;
5use std::path::PathBuf;
6
7use serde::Deserialize;
8use serde::Serialize;
9
10use crate::diagnostic::Warning;
11use crate::diagnostic::pointer;
12use crate::diagnostic::report_warnings;
13use crate::error::Error;
14use crate::error::Result;
15
16#[derive(Debug, Default, Clone, Deserialize, Serialize)]
20#[serde(rename_all = "kebab-case")]
21pub struct Config {
22 pub package: Option<String>,
24 pub output: Option<PathBuf>,
26 #[serde(default)]
28 pub generate: Generate,
29 #[serde(default)]
31 pub output_options: OutputOptions,
32 #[serde(default)]
34 pub import_mapping: BTreeMap<String, String>,
35}
36
37#[derive(Debug, Default, Clone, Deserialize, Serialize)]
39#[serde(rename_all = "kebab-case")]
40pub struct Generate {
41 #[serde(default)]
43 pub models: bool,
44 #[serde(default)]
46 pub std_http_server: bool,
47 #[serde(default)]
49 pub client: bool,
50 #[serde(default)]
52 pub embedded_spec: bool,
53 #[serde(default)]
55 pub server_urls: bool,
56}
57
58pub(crate) const OUTPUT_OPTIONS_KEY: &str = "output-options";
62
63pub(crate) const RESPONSE_TYPE_SUFFIX_KEY: &str = "response-type-suffix";
67
68pub(crate) const DEFAULT_RESPONSE_SUFFIX: &str = "response";
73
74pub(crate) const TYPE_NAME_SUFFIX_KEY: &str = "type-name-suffix";
78
79#[derive(Debug, Default, Clone, Deserialize, Serialize)]
81#[serde(rename_all = "kebab-case")]
82pub struct OutputOptions {
83 #[serde(default)]
85 pub skip_prune: bool,
86 #[serde(default)]
88 pub include_tags: Vec<String>,
89 #[serde(default)]
91 pub exclude_tags: Vec<String>,
92 #[serde(default)]
95 pub include_operation_ids: Vec<String>,
96 #[serde(default)]
98 pub exclude_operation_ids: Vec<String>,
99 #[serde(default)]
104 pub exclude_schemas: Vec<String>,
105 #[serde(default)]
110 pub response_type_suffix: Option<String>,
111 #[serde(default)]
125 pub type_name_suffix: Option<String>,
126}
127
128impl Config {
129 pub fn load(path: &Path) -> Result<Self> {
131 let text = std::fs::read_to_string(path).map_err(|source| {
132 return Error::ReadConfig {
133 path: path.display().to_string(),
134 source,
135 };
136 })?;
137 let value: serde_yaml::Value = serde_yaml::from_str(&text).map_err(|source| {
138 return Error::ParseConfig {
139 path: path.display().to_string(),
140 source,
141 };
142 })?;
143 let warnings = configuration_warnings(&value).map_err(|source| {
144 return Error::ParseConfig {
145 path: path.display().to_string(),
146 source,
147 };
148 })?;
149 report_warnings(&path.display().to_string(), &warnings);
150 let config: Config = serde_yaml::from_str(&text).map_err(|source| {
151 return Error::ParseConfig {
152 path: path.display().to_string(),
153 source,
154 };
155 })?;
156 return Ok(config);
157 }
158}
159
160fn configuration_warnings(value: &serde_yaml::Value) -> serde_yaml::Result<Vec<Warning>> {
161 let shape = serde_yaml::to_value(Config::default())?;
162 let mut warnings = Vec::new();
163 collect_unknown_keys(value, &shape, "", &mut warnings);
164 return Ok(warnings);
165}
166
167fn collect_unknown_keys(
168 value: &serde_yaml::Value,
169 shape: &serde_yaml::Value,
170 parent: &str,
171 warnings: &mut Vec<Warning>,
172) {
173 let (Some(mapping), Some(fields)) = (value.as_mapping(), shape.as_mapping()) else {
174 return;
175 };
176 if fields.is_empty() {
178 return;
179 }
180 for (key, value) in mapping {
181 let Some(name) = key.as_str() else {
182 continue;
183 };
184 let path = pointer(parent, name);
185 if let Some(field) = fields.get(key) {
186 collect_unknown_keys(value, field, &path, warnings);
187 } else {
188 warnings.push(Warning::new(path, "unknown configuration key is ignored"));
189 }
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn unknown_configuration_keys_warn() -> serde_yaml::Result<()> {
199 for (yaml, paths) in [
200 ("packge: demo", vec!["/packge"]),
201 ("generate: {modles: true}", vec!["/generate/modles"]),
202 ("output-options: {skip-prun: true}", vec!["/output-options/skip-prun"]),
203 ("a~/b: secret", vec!["/a~0~1b"]),
204 ("generate: {'~1/': true}", vec!["/generate/~01~1"]),
205 (
206 "packge: demo\ngenerate: {modles: true}\noutput-options: {skip-prun: true}",
207 vec!["/packge", "/generate/modles", "/output-options/skip-prun"],
208 ),
209 ] {
210 let value = serde_yaml::from_str(yaml)?;
211 let expected: Vec<_> = paths
212 .into_iter()
213 .map(|path| {
214 return Warning::new(path, "unknown configuration key is ignored");
215 })
216 .collect();
217 assert_eq!(configuration_warnings(&value)?, expected, "{yaml}");
218 assert!(serde_yaml::from_value::<Config>(value).is_ok(), "{yaml}");
219 }
220 return Ok(());
221 }
222
223 #[test]
224 fn known_configuration_keys_do_not_warn() -> serde_yaml::Result<()> {
225 for yaml in [
226 "{}",
227 "package: demo\noutput: generated.rs",
228 "generate: {models: true, std-http-server: true, client: true, embedded-spec: false, server-urls: true}",
229 "import-mapping: {'./other.yaml': other, 'a~/b': module}",
230 "output-options: {include-tags: [pets], response-type-suffix: Resp, type-name-suffix: Alt}",
231 ] {
232 let value = serde_yaml::from_str(yaml)?;
233 assert!(configuration_warnings(&value)?.is_empty(), "{yaml}");
234 assert!(serde_yaml::from_value::<Config>(value).is_ok(), "{yaml}");
235 }
236 let defaults = serde_yaml::to_value(Config::default())?;
237 assert!(configuration_warnings(&defaults)?.is_empty());
238 return Ok(());
239 }
240
241 #[test]
242 fn invalid_configuration_values_remain_deserialization_errors() -> serde_yaml::Result<()> {
243 for yaml in ["null", "generate: null", "output-options: null", "import-mapping: null"] {
244 let value = serde_yaml::from_str(yaml)?;
245 assert!(configuration_warnings(&value)?.is_empty(), "{yaml}");
246 }
247 for yaml in [
248 "false",
249 "[]",
250 "generate: wrong",
251 "generate: {models: wrong}",
252 "output-options: []",
253 "output-options: {include-tags: false}",
254 "import-mapping: {other: []}",
255 ] {
256 let value = serde_yaml::from_str(yaml)?;
257 assert!(configuration_warnings(&value)?.is_empty(), "{yaml}");
258 assert!(serde_yaml::from_value::<Config>(value).is_err(), "{yaml}");
259 }
260 let value = serde_yaml::from_str("generate: {modles: true, models: wrong}")?;
261 assert_eq!(
262 configuration_warnings(&value)?,
263 vec![Warning::new("/generate/modles", "unknown configuration key is ignored")]
264 );
265 assert!(serde_yaml::from_value::<Config>(value).is_err());
266 return Ok(());
267 }
268
269 #[test]
270 fn config_keys_match_serde_names() {
271 let yaml =
272 format!("{OUTPUT_OPTIONS_KEY}:\n {RESPONSE_TYPE_SUFFIX_KEY}: Resp\n {TYPE_NAME_SUFFIX_KEY}: Alt\n",);
273 let config: Config = serde_yaml::from_str(&yaml).expect("config parses");
274 assert_eq!(
275 config.output_options.response_type_suffix.as_deref(),
276 Some("Resp"),
277 "OUTPUT_OPTIONS_KEY/RESPONSE_TYPE_SUFFIX_KEY drifted from the serde field names",
278 );
279 assert_eq!(
280 config.output_options.type_name_suffix.as_deref(),
281 Some("Alt"),
282 "TYPE_NAME_SUFFIX_KEY drifted from the serde field name",
283 );
284 }
285
286 #[test]
287 fn type_name_suffix_defaults_to_unset() {
288 let config: Config = serde_yaml::from_str("package: demo\n").expect("config parses");
291 assert_eq!(config.output_options.type_name_suffix, None);
292 }
293
294 #[test]
295 fn default_response_suffix_pascalizes_to_response() {
296 use crate::naming::Case;
297 use crate::naming::to_ident;
298
299 assert_eq!(to_ident(DEFAULT_RESPONSE_SUFFIX, Case::Pascal).logical(), "Response");
300 }
301}