use quote::{format_ident, quote};
use syn::{parenthesized, punctuated::Punctuated, Attribute, Expr, Ident, Path, Token};
#[derive(Debug)]
pub enum CustomDe {
Fn(Path),
Mod(Path),
}
pub fn quote_custom_serde_payload_field(
field_id: &Ident,
ty: &syn::Type,
custom_de: CustomDe,
option: bool,
) -> (Ident, proc_macro2::TokenStream) {
let (id, fn_path) = match custom_de {
CustomDe::Fn(ref p) => (p.segments.last().unwrap(), p),
CustomDe::Mod(ref p) => (p.segments.last().unwrap(), p),
};
let custom_fn_id = format_ident!("{}_{field_id}_payload", id.ident);
let module_de = matches!(custom_de, CustomDe::Mod(_)).then_some(quote!(::deserialize));
let res = if option {
quote!(res)
} else {
quote!(Some(res))
};
let ty = if option {
quote!(#ty)
} else {
quote!(Option<#ty>)
};
let tokens = quote!(
fn #custom_fn_id<'de, D>(deserializer: D) -> Result<#ty, D::Error>
where
D: serde::Deserializer<'de>
{
match #fn_path #module_de (deserializer) {
Ok(res) => Ok(#res),
Err(e) => Err(e)
}
}
);
(custom_fn_id, tokens)
}
pub fn extract_custom_serde<'a>(
serde_attrs: &[&'a Attribute],
) -> (Option<CustomDe>, Vec<&'a Attribute>) {
let mut custom_fn = None;
let mut rest = vec![];
for attr in serde_attrs {
if custom_fn.is_some() {
rest.push(*attr);
continue;
}
let metas = attr.meta.require_list().unwrap();
let parsed = metas.parse_nested_meta(|meta| {
if meta.path.is_ident("deserialize_with") && meta.input.peek(Token!(=)) {
let content = meta.value()?;
if let Ok(lit) = content.parse::<syn::LitStr>() {
custom_fn = Some(CustomDe::Fn(syn::parse_str::<Path>(&lit.value())?));
return Ok(());
}
}
if meta.path.is_ident("with") && meta.input.peek(Token!(=)) {
let content = meta.value()?;
if let Ok(lit) = content.parse::<syn::LitStr>() {
custom_fn = Some(CustomDe::Mod(syn::parse_str::<Path>(&lit.value())?));
return Ok(());
}
}
Err(meta.error("will get caught"))
});
if parsed.is_err() {
rest.push(*attr);
continue;
};
}
(custom_fn, rest)
}
pub fn find_rename_all(attrs: &[syn::Attribute]) -> Option<RenameRule> {
let mut rule = None;
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
let Ok(metas) = attr.meta.require_list() else {
continue;
};
let parsed = metas.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") && meta.input.peek(Token!(=)) {
let content = meta.value()?;
if let Ok(lit) = content.parse::<syn::LitStr>() {
rule = RenameRule::from_str(&lit.value());
return Ok(());
}
}
if meta.input.peek(syn::token::Paren) {
let content;
parenthesized!(content in meta.input);
let name_values =
Punctuated::<syn::MetaNameValue, Token![,]>::parse_separated_nonempty(
&content,
)?;
for pair in name_values.pairs() {
let name_value = pair.into_value();
if name_value.path.is_ident("deserialize") {
let Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(ref lit),
..
}) = name_value.value
else {
return Ok(());
};
rule = RenameRule::from_str(&lit.value());
}
}
return Ok(());
}
Ok(())
});
if parsed.is_err() {
continue;
}
}
rule
}
pub fn find_rename(field: &syn::Field) -> Option<String> {
let mut original_name = None;
for attr in field.attrs.iter() {
if !attr.path().is_ident("serde") {
continue;
}
let Ok(serde_meta) = attr.meta.require_list() else {
continue;
};
let parsed = serde_meta.parse_nested_meta(|meta| {
if !meta.path.is_ident("rename") {
return Ok(());
}
if meta.input.peek(Token!(=)) {
let content = meta.value()?;
original_name = Some(content.parse::<syn::LitStr>()?.value());
return Ok(());
}
if meta.input.peek(syn::token::Paren) {
let content;
parenthesized!(content in meta.input);
let name_values =
Punctuated::<syn::MetaNameValue, Token![,]>::parse_separated_nonempty(
&content,
)?;
for pair in name_values.pairs() {
let name_value = pair.into_value();
if name_value.path.is_ident("deserialize") {
let Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(ref lit),
..
}) = name_value.value
else {
return Ok(());
};
original_name = Some(lit.value())
}
}
return Ok(());
}
Ok(())
});
if parsed.is_err() {
continue;
}
}
original_name
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum RenameRule {
Lower,
Upper,
Pascal,
Camel,
Snake,
ScreamingSnake,
Kebab,
ScreamingKebab,
}
impl RenameRule {
pub fn from_str(rename_all_str: &str) -> Option<Self> {
for (name, rule) in RENAME_RULES {
if rename_all_str == *name {
return Some(*rule);
}
}
None
}
pub fn apply_to_field(self, field: &str) -> String {
use RenameRule as RR;
match self {
RR::Lower | RR::Snake => field.to_owned(),
RR::Upper => field.to_ascii_uppercase(),
RR::Pascal => {
let mut pascal = String::new();
let mut capitalize = true;
for ch in field.chars() {
if ch == '_' {
capitalize = true;
} else if capitalize {
pascal.push(ch.to_ascii_uppercase());
capitalize = false;
} else {
pascal.push(ch);
}
}
pascal
}
RR::Camel => {
let pascal = RR::Pascal.apply_to_field(field);
pascal[..1].to_ascii_lowercase() + &pascal[1..]
}
RR::ScreamingSnake => field.to_ascii_uppercase(),
RR::Kebab => field.replace('_', "-"),
RR::ScreamingKebab => RR::ScreamingSnake.apply_to_field(field).replace('_', "-"),
}
}
}
static RENAME_RULES: &[(&str, RenameRule)] = &[
("lowercase", RenameRule::Lower),
("UPPERCASE", RenameRule::Upper),
("PascalCase", RenameRule::Pascal),
("camelCase", RenameRule::Camel),
("snake_case", RenameRule::Snake),
("SCREAMING_SNAKE_CASE", RenameRule::ScreamingSnake),
("kebab-case", RenameRule::Kebab),
("SCREAMING-KEBAB-CASE", RenameRule::ScreamingKebab),
];