use proc_macro2::TokenStream;
#[derive(Debug, Clone)]
pub struct TaggedEnumInfo {
pub tag_field: String,
pub content_field: Option<String>,
pub rename_all: Option<String>,
}
pub fn has_union_attribute(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
if attr.path().is_ident("llm") {
if let Ok(meta_list) = attr.meta.require_list() {
return meta_list.tokens.to_string().trim() == "union";
}
}
false
})
}
pub fn has_untagged_attribute(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
if attr.path().is_ident("serde") {
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("untagged") {
found = true;
}
Ok(())
});
return found;
}
false
})
}
pub fn extract_tag_info(attrs: &[syn::Attribute]) -> Result<Option<TaggedEnumInfo>, TokenStream> {
let mut tag_field: Option<String> = None;
let mut content_field: Option<String> = None;
let mut rename_all: Option<String> = None;
let mut rename_all_lit: Option<syn::LitStr> = None;
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("tag") {
let value = meta.value()?;
match value.parse::<syn::Expr>() {
Ok(syn::Expr::Lit(expr_lit)) => {
if let syn::Lit::Str(lit_str) = &expr_lit.lit {
tag_field = Some(lit_str.value());
} else {
return Err(syn::Error::new_spanned(
&expr_lit.lit,
"tag attribute value must be a string literal",
));
}
}
Ok(syn::Expr::Path(_path)) => {
return Err(syn::Error::new_spanned(
meta.path,
"tag attribute must be a string literal, const paths are not supported",
));
}
Ok(other) => {
return Err(syn::Error::new_spanned(
other,
"tag attribute value must be a string literal",
));
}
Err(e) => return Err(e),
}
} else if meta.path.is_ident("content") {
let value = meta.value()?;
match value.parse::<syn::Expr>() {
Ok(syn::Expr::Lit(expr_lit)) => {
if let syn::Lit::Str(lit_str) = &expr_lit.lit {
content_field = Some(lit_str.value());
} else {
return Err(syn::Error::new_spanned(
&expr_lit.lit,
"content attribute value must be a string literal",
));
}
}
Ok(syn::Expr::Path(_path)) => {
return Err(syn::Error::new_spanned(
meta.path,
"content attribute must be a string literal, const paths are not supported",
));
}
Ok(other) => {
return Err(syn::Error::new_spanned(
other,
"content attribute value must be a string literal",
));
}
Err(e) => return Err(e),
}
} else if meta.path.is_ident("rename_all") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
rename_all = Some(lit.value());
rename_all_lit = Some(lit);
}
Ok(())
})
.map_err(|e| e.to_compile_error())?;
}
if let Some(rule) = &rename_all {
let valid = [
"snake_case",
"camelCase",
"PascalCase",
"kebab-case",
"SCREAMING_SNAKE_CASE",
];
if !valid.contains(&rule.as_str()) {
if let Some(lit) = rename_all_lit {
let error = syn::Error::new_spanned(
lit,
format!(
"Invalid rename_all value: '{}'. Valid values: {}",
rule,
valid.join(", ")
),
);
return Err(error.to_compile_error());
}
}
}
Ok(tag_field.map(|tag| TaggedEnumInfo {
tag_field: tag,
content_field,
rename_all,
}))
}
pub fn apply_rename_all_at_compile_time(s: &str, rule: &str) -> String {
match rule {
"snake_case" => {
let mut result = String::new();
for ch in s.chars() {
if ch.is_uppercase() {
if !result.is_empty() {
result.push('_');
}
result.push(ch.to_ascii_lowercase());
} else {
result.push(ch);
}
}
result
}
"camelCase" => {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
}
}
"PascalCase" => {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
"SCREAMING_SNAKE_CASE" => {
let mut result = String::new();
for ch in s.chars() {
if ch.is_uppercase() && !result.is_empty() {
result.push('_');
}
result.push(ch.to_ascii_uppercase());
}
result
}
"kebab-case" => {
let mut result = String::new();
for ch in s.chars() {
if ch.is_uppercase() {
if !result.is_empty() {
result.push('-');
}
result.push(ch.to_ascii_lowercase());
} else {
result.push(ch);
}
}
result
}
_ => s.to_string(),
}
}
pub fn validate_variant_rename_all(variant: &syn::Variant) -> Result<(), TokenStream> {
for attr in &variant.attrs {
if attr.path().is_ident("serde") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
let rule = lit.value();
let valid = [
"snake_case",
"camelCase",
"PascalCase",
"kebab-case",
"SCREAMING_SNAKE_CASE",
];
if !valid.contains(&rule.as_str()) {
return Err(syn::Error::new_spanned(
lit,
format!(
"Invalid rename_all value: '{}'. Valid values: {}",
rule,
valid.join(", ")
),
));
}
}
Ok(())
})
.map_err(|e| e.to_compile_error())?;
}
}
Ok(())
}