use syn::{Fields, Ident, ItemStruct, LitStr, Result};
const FIELD_ATTRIBUTE: &str = "config";
pub(super) fn field_names(input: &ItemStruct) -> Option<Vec<String>> {
let Fields::Named(fields) = &input.fields else {
return None;
};
let mut names = Vec::new();
for field in &fields.named {
let mut renamed = None;
let mut aliases = Vec::new();
let mut flattened = false;
for attribute in &field.attrs {
if !attribute.path().is_ident("serde") {
continue;
}
let _ = attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("flatten") {
flattened = true;
}
if meta.path.is_ident("alias") {
if let Ok(value) = meta.value() {
if let Ok(name) = value.parse::<LitStr>() {
aliases.push(name.value());
}
}
}
if meta.path.is_ident("rename") {
if let Ok(value) = meta.value() {
if let Ok(name) = value.parse::<LitStr>() {
renamed = Some(name.value());
}
}
}
Ok(())
});
}
if flattened {
return None;
}
names.push(renamed.unwrap_or_else(|| {
field
.ident
.as_ref()
.expect("named fields always have an identifier")
.to_string()
}));
names.append(&mut aliases);
}
Some(names)
}
pub(super) fn take_field_options(input: &mut ItemStruct) -> Result<Vec<(Ident, String)>> {
let rename_all = container_rename_all(input);
let Fields::Named(fields) = &mut input.fields else {
return Ok(Vec::new());
};
let mut secrets = Vec::new();
for field in &mut fields.named {
let mut is_secret = false;
let mut error = None;
field.attrs.retain(|attribute| {
if !attribute.path().is_ident(FIELD_ATTRIBUTE) {
return true;
}
let parsed = attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("secret") {
is_secret = true;
return Ok(());
}
Err(meta.error("unknown option; the only one is `secret`"))
});
if let Err(parse_error) = parsed {
error.get_or_insert(parse_error);
}
false
});
if let Some(error) = error {
return Err(error);
}
if is_secret {
let ident = field
.ident
.clone()
.expect("named fields always have an identifier");
let serde_name = field_rename(field)
.or_else(|| {
rename_all
.as_deref()
.map(|rule| apply_rename_all(rule, &ident.to_string()))
})
.unwrap_or_else(|| ident.to_string());
secrets.push((ident, serde_name));
}
}
Ok(secrets)
}
fn container_rename_all(input: &ItemStruct) -> Option<String> {
for attribute in &input.attrs {
if !attribute.path().is_ident("serde") {
continue;
}
let mut found = None;
let _ = attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") {
if let Ok(value) = meta.value() {
if let Ok(rule) = value.parse::<LitStr>() {
found = Some(rule.value());
}
}
}
Ok(())
});
if found.is_some() {
return found;
}
}
None
}
fn field_rename(field: &syn::Field) -> Option<String> {
for attribute in &field.attrs {
if !attribute.path().is_ident("serde") {
continue;
}
let mut found = None;
let _ = attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
if let Ok(value) = meta.value() {
if let Ok(name) = value.parse::<LitStr>() {
found = Some(name.value());
}
}
}
Ok(())
});
if found.is_some() {
return found;
}
}
None
}
fn apply_rename_all(rule: &str, name: &str) -> String {
let words: Vec<&str> = name.split('_').filter(|word| !word.is_empty()).collect();
let capitalize = |word: &str| {
let mut chars = word.chars();
chars.next().map_or_else(String::new, |first| {
first.to_uppercase().collect::<String>() + chars.as_str()
})
};
match rule {
"lowercase" => name.to_lowercase(),
"UPPERCASE" => name.to_uppercase(),
"PascalCase" => words.iter().map(|word| capitalize(word)).collect(),
"camelCase" => {
let mut out = String::new();
for (index, word) in words.iter().enumerate() {
if index == 0 {
out.push_str(word);
} else {
out.push_str(&capitalize(word));
}
}
out
}
"SCREAMING_SNAKE_CASE" => name.to_uppercase(),
"kebab-case" => name.replace('_', "-"),
"SCREAMING-KEBAB-CASE" => name.to_uppercase().replace('_', "-"),
_ => name.to_owned(),
}
}