use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
use serde::Deserialize;
use serde::Serialize;
use crate::diagnostic::Warning;
use crate::diagnostic::pointer;
use crate::diagnostic::report_warnings;
use crate::error::Error;
use crate::error::Result;
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
pub package: Option<String>,
pub output: Option<PathBuf>,
#[serde(default)]
pub generate: Generate,
#[serde(default)]
pub output_options: OutputOptions,
#[serde(default)]
pub import_mapping: BTreeMap<String, String>,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Generate {
#[serde(default)]
pub models: bool,
#[serde(default)]
pub std_http_server: bool,
#[serde(default)]
pub client: bool,
#[serde(default)]
pub embedded_spec: bool,
#[serde(default)]
pub server_urls: bool,
}
pub(crate) const OUTPUT_OPTIONS_KEY: &str = "output-options";
pub(crate) const RESPONSE_TYPE_SUFFIX_KEY: &str = "response-type-suffix";
pub(crate) const DEFAULT_RESPONSE_SUFFIX: &str = "response";
pub(crate) const TYPE_NAME_SUFFIX_KEY: &str = "type-name-suffix";
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct OutputOptions {
#[serde(default)]
pub skip_prune: bool,
#[serde(default)]
pub include_tags: Vec<String>,
#[serde(default)]
pub exclude_tags: Vec<String>,
#[serde(default)]
pub include_operation_ids: Vec<String>,
#[serde(default)]
pub exclude_operation_ids: Vec<String>,
#[serde(default)]
pub exclude_schemas: Vec<String>,
#[serde(default)]
pub response_type_suffix: Option<String>,
#[serde(default)]
pub type_name_suffix: Option<String>,
}
impl Config {
pub fn load(path: &Path) -> Result<Self> {
let text = std::fs::read_to_string(path).map_err(|source| {
return Error::ReadConfig {
path: path.display().to_string(),
source,
};
})?;
let value: serde_yaml::Value = serde_yaml::from_str(&text).map_err(|source| {
return Error::ParseConfig {
path: path.display().to_string(),
source,
};
})?;
let warnings = configuration_warnings(&value).map_err(|source| {
return Error::ParseConfig {
path: path.display().to_string(),
source,
};
})?;
report_warnings(&path.display().to_string(), &warnings);
let config: Config = serde_yaml::from_str(&text).map_err(|source| {
return Error::ParseConfig {
path: path.display().to_string(),
source,
};
})?;
return Ok(config);
}
}
fn configuration_warnings(value: &serde_yaml::Value) -> serde_yaml::Result<Vec<Warning>> {
let shape = serde_yaml::to_value(Config::default())?;
let mut warnings = Vec::new();
collect_unknown_keys(value, &shape, "", &mut warnings);
return Ok(warnings);
}
fn collect_unknown_keys(
value: &serde_yaml::Value,
shape: &serde_yaml::Value,
parent: &str,
warnings: &mut Vec<Warning>,
) {
let (Some(mapping), Some(fields)) = (value.as_mapping(), shape.as_mapping()) else {
return;
};
if fields.is_empty() {
return;
}
for (key, value) in mapping {
let Some(name) = key.as_str() else {
continue;
};
let path = pointer(parent, name);
if let Some(field) = fields.get(key) {
collect_unknown_keys(value, field, &path, warnings);
} else {
warnings.push(Warning::new(path, "unknown configuration key is ignored"));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_configuration_keys_warn() -> serde_yaml::Result<()> {
for (yaml, paths) in [
("packge: demo", vec!["/packge"]),
("generate: {modles: true}", vec!["/generate/modles"]),
("output-options: {skip-prun: true}", vec!["/output-options/skip-prun"]),
("a~/b: secret", vec!["/a~0~1b"]),
("generate: {'~1/': true}", vec!["/generate/~01~1"]),
(
"packge: demo\ngenerate: {modles: true}\noutput-options: {skip-prun: true}",
vec!["/packge", "/generate/modles", "/output-options/skip-prun"],
),
] {
let value = serde_yaml::from_str(yaml)?;
let expected: Vec<_> = paths
.into_iter()
.map(|path| {
return Warning::new(path, "unknown configuration key is ignored");
})
.collect();
assert_eq!(configuration_warnings(&value)?, expected, "{yaml}");
assert!(serde_yaml::from_value::<Config>(value).is_ok(), "{yaml}");
}
return Ok(());
}
#[test]
fn known_configuration_keys_do_not_warn() -> serde_yaml::Result<()> {
for yaml in [
"{}",
"package: demo\noutput: generated.rs",
"generate: {models: true, std-http-server: true, client: true, embedded-spec: false, server-urls: true}",
"import-mapping: {'./other.yaml': other, 'a~/b': module}",
"output-options: {include-tags: [pets], response-type-suffix: Resp, type-name-suffix: Alt}",
] {
let value = serde_yaml::from_str(yaml)?;
assert!(configuration_warnings(&value)?.is_empty(), "{yaml}");
assert!(serde_yaml::from_value::<Config>(value).is_ok(), "{yaml}");
}
let defaults = serde_yaml::to_value(Config::default())?;
assert!(configuration_warnings(&defaults)?.is_empty());
return Ok(());
}
#[test]
fn invalid_configuration_values_remain_deserialization_errors() -> serde_yaml::Result<()> {
for yaml in ["null", "generate: null", "output-options: null", "import-mapping: null"] {
let value = serde_yaml::from_str(yaml)?;
assert!(configuration_warnings(&value)?.is_empty(), "{yaml}");
}
for yaml in [
"false",
"[]",
"generate: wrong",
"generate: {models: wrong}",
"output-options: []",
"output-options: {include-tags: false}",
"import-mapping: {other: []}",
] {
let value = serde_yaml::from_str(yaml)?;
assert!(configuration_warnings(&value)?.is_empty(), "{yaml}");
assert!(serde_yaml::from_value::<Config>(value).is_err(), "{yaml}");
}
let value = serde_yaml::from_str("generate: {modles: true, models: wrong}")?;
assert_eq!(
configuration_warnings(&value)?,
vec![Warning::new("/generate/modles", "unknown configuration key is ignored")]
);
assert!(serde_yaml::from_value::<Config>(value).is_err());
return Ok(());
}
#[test]
fn config_keys_match_serde_names() {
let yaml =
format!("{OUTPUT_OPTIONS_KEY}:\n {RESPONSE_TYPE_SUFFIX_KEY}: Resp\n {TYPE_NAME_SUFFIX_KEY}: Alt\n",);
let config: Config = serde_yaml::from_str(&yaml).expect("config parses");
assert_eq!(
config.output_options.response_type_suffix.as_deref(),
Some("Resp"),
"OUTPUT_OPTIONS_KEY/RESPONSE_TYPE_SUFFIX_KEY drifted from the serde field names",
);
assert_eq!(
config.output_options.type_name_suffix.as_deref(),
Some("Alt"),
"TYPE_NAME_SUFFIX_KEY drifted from the serde field name",
);
}
#[test]
fn type_name_suffix_defaults_to_unset() {
let config: Config = serde_yaml::from_str("package: demo\n").expect("config parses");
assert_eq!(config.output_options.type_name_suffix, None);
}
#[test]
fn default_response_suffix_pascalizes_to_response() {
use crate::naming::Case;
use crate::naming::to_ident;
assert_eq!(to_ident(DEFAULT_RESPONSE_SUFFIX, Case::Pascal).logical(), "Response");
}
}