deser-derive 0.8.0

Derive extension for deser
Documentation
use std::borrow::Cow;

#[derive(Copy, Clone)]
#[allow(clippy::enum_variant_names)]
pub enum RenameAll {
    LowerCase,
    UpperCase,
    PascalCase,
    CamelCase,
    SnakeCase,
    ScreamingSnakeCase,
    KebabCase,
    ScreamingKebabCase,
}

impl RenameAll {
    fn parse(lit: &syn::Lit) -> Result<RenameAll, syn::Error> {
        if let syn::Lit::Str(s) = &lit {
            match s.value().as_str() {
                "lowercase" => Ok(RenameAll::LowerCase),
                "UPPERCASE" => Ok(RenameAll::UpperCase),
                "PascalCase" => Ok(RenameAll::PascalCase),
                "camelCase" => Ok(RenameAll::CamelCase),
                "snake_case" => Ok(RenameAll::SnakeCase),
                "SCREAMING_SNAKE_CASE" => Ok(RenameAll::ScreamingSnakeCase),
                "kebab-case" => Ok(RenameAll::KebabCase),
                "SCREAMING-KEBAB-CASE" => Ok(RenameAll::ScreamingKebabCase),
                _ => Err(syn::Error::new_spanned(lit, "")),
            }
        } else {
            Err(syn::Error::new_spanned(lit, "rename expects a string"))
        }
    }
}

#[derive(Clone)]
pub enum TypeDefault {
    Implicit,
    Explicit(syn::ExprPath),
}

pub struct ContainerAttrs<'a> {
    ident: &'a syn::Ident,
    rename: Option<String>,
    rename_all: Option<RenameAll>,
    default: Option<TypeDefault>,
    skip_serializing_optionals: bool,
}

pub fn get_meta_items(attr: &syn::Attribute) -> syn::Result<Vec<syn::NestedMeta>> {
    if !attr.path.is_ident("deser") {
        return Ok(Vec::new());
    }

    match attr.parse_meta() {
        Ok(syn::Meta::List(meta)) => Ok(meta.nested.into_iter().collect()),
        Ok(_) => Err(syn::Error::new_spanned(attr, "expected #[deser(...)]")),
        Err(err) => Err(err),
    }
}

fn respan(stream: proc_macro2::TokenStream, span: proc_macro2::Span) -> proc_macro2::TokenStream {
    stream
        .into_iter()
        .map(|mut token| {
            if let proc_macro2::TokenTree::Group(g) = &mut token {
                *g = proc_macro2::Group::new(g.delimiter(), respan(g.stream(), span));
            }
            token.set_span(span);
            token
        })
        .collect()
}

fn get_lit_str(attr_name: &str, lit: &syn::Lit) -> syn::Result<String> {
    if let syn::Lit::Str(lit) = lit {
        Ok(lit.value())
    } else {
        Err(syn::Error::new_spanned(
            lit,
            format!(
                "expected attribute to be a string: `{} = \"...\"`",
                attr_name,
            ),
        ))
    }
}

fn parse_lit_into_expr_path(attr_name: &str, lit: &syn::Lit) -> syn::Result<syn::ExprPath> {
    let string = get_lit_str(attr_name, lit)?;
    let token_stream = syn::parse_str(&string)?;
    syn::parse2(respan(token_stream, lit.span()))
}

impl<'a> ContainerAttrs<'a> {
    pub fn of(input: &'a syn::DeriveInput) -> syn::Result<ContainerAttrs<'a>> {
        let mut rv = ContainerAttrs {
            ident: &input.ident,
            rename: None,
            rename_all: None,
            default: None,
            skip_serializing_optionals: false,
        };

        for meta_item in input.attrs.iter().flat_map(get_meta_items).flatten() {
            if let syn::NestedMeta::Meta(meta) = meta_item {
                match &meta {
                    syn::Meta::NameValue(nv) if nv.path.is_ident("rename_all") => {
                        if rv.rename_all.is_some() {
                            return Err(syn::Error::new_spanned(
                                meta,
                                "duplicate rename_all attribute",
                            ));
                        }
                        rv.rename_all = Some(RenameAll::parse(&nv.lit)?);
                    }
                    syn::Meta::NameValue(nv) if nv.path.is_ident("rename") => {
                        if rv.rename.is_some() {
                            return Err(syn::Error::new_spanned(
                                meta,
                                "duplicate rename attribute",
                            ));
                        }
                        rv.rename = Some(get_lit_str("rename", &nv.lit)?);
                    }
                    syn::Meta::Path(path) if path.is_ident("default") => {
                        if rv.default.is_some() {
                            return Err(syn::Error::new_spanned(
                                meta,
                                "duplicate default attribute",
                            ));
                        }
                        rv.default = Some(TypeDefault::Implicit);
                    }
                    syn::Meta::Path(path) if path.is_ident("skip_serializing_optionals") => {
                        if rv.skip_serializing_optionals {
                            return Err(syn::Error::new_spanned(
                                meta,
                                "duplicate skip_serializing_optionals attribute",
                            ));
                        }
                        rv.skip_serializing_optionals = true;
                    }
                    syn::Meta::NameValue(nv) if nv.path.is_ident("default") => {
                        if rv.default.is_some() {
                            return Err(syn::Error::new_spanned(
                                meta,
                                "duplicate default attribute",
                            ));
                        }
                        rv.default = Some(TypeDefault::Explicit(parse_lit_into_expr_path(
                            "default", &nv.lit,
                        )?));
                    }
                    _ => return Err(syn::Error::new_spanned(meta, "unsupported attribute")),
                }
            } else {
                return Err(syn::Error::new_spanned(meta_item, "unsupported attribute"));
            }
        }

        Ok(rv)
    }

    pub fn container_name(&self) -> String {
        match self.rename {
            Some(ref name) => name.clone(),
            None => self.ident.to_string(),
        }
    }

    pub fn get_field_name(&self, field: &syn::Field) -> String {
        let name = field.ident.as_ref().unwrap().to_string();
        if let Some(rename_all) = self.rename_all {
            match rename_all {
                RenameAll::LowerCase | RenameAll::SnakeCase => name,
                RenameAll::UpperCase | RenameAll::ScreamingSnakeCase => name.to_ascii_uppercase(),
                RenameAll::PascalCase => {
                    let mut pascal = String::new();
                    let mut capitalize = true;
                    for ch in name.chars() {
                        if ch == '_' {
                            capitalize = true;
                        } else if capitalize {
                            pascal.push(ch.to_ascii_uppercase());
                            capitalize = false;
                        } else {
                            pascal.push(ch);
                        }
                    }
                    pascal
                }
                RenameAll::CamelCase => {
                    let mut camel = String::new();
                    let mut capitalize = false;
                    for ch in name.chars() {
                        if ch == '_' {
                            capitalize = true;
                        } else if capitalize {
                            camel.push(ch.to_ascii_uppercase());
                            capitalize = false;
                        } else {
                            camel.push(ch);
                        }
                    }
                    camel
                }
                RenameAll::KebabCase => name.replace("_", "-"),
                RenameAll::ScreamingKebabCase => name.replace("_", "-").to_ascii_uppercase(),
            }
        } else {
            name
        }
    }

    pub fn default(&self) -> Option<&TypeDefault> {
        self.default.as_ref()
    }

    pub fn skip_serializing_optionals(&self) -> bool {
        self.skip_serializing_optionals
    }

    pub fn get_variant_name(&self, variant: &syn::Variant) -> String {
        let name = variant.ident.to_string();
        if let Some(rename_all) = self.rename_all {
            match rename_all {
                RenameAll::PascalCase => name,
                RenameAll::LowerCase => name.to_ascii_lowercase(),
                RenameAll::UpperCase => name.to_ascii_uppercase(),
                RenameAll::CamelCase => name[..1].to_ascii_lowercase() + &name[1..],
                RenameAll::SnakeCase
                | RenameAll::ScreamingSnakeCase
                | RenameAll::KebabCase
                | RenameAll::ScreamingKebabCase => {
                    let sep = if matches!(
                        rename_all,
                        RenameAll::SnakeCase | RenameAll::ScreamingSnakeCase
                    ) {
                        '_'
                    } else {
                        '-'
                    };
                    let upper = matches!(
                        rename_all,
                        RenameAll::ScreamingKebabCase | RenameAll::ScreamingSnakeCase
                    );
                    let mut rv = String::new();
                    for (i, ch) in name.char_indices() {
                        if i > 0 && ch.is_uppercase() {
                            rv.push(sep);
                        }
                        rv.push(if upper {
                            ch.to_ascii_uppercase()
                        } else {
                            ch.to_ascii_lowercase()
                        });
                    }
                    rv
                }
            }
        } else {
            name
        }
    }
}

pub fn ensure_no_field_attrs(field: &syn::Field) -> syn::Result<()> {
    if let Some(first) = field.attrs.iter().flat_map(get_meta_items).flatten().next() {
        Err(syn::Error::new_spanned(first, "unsupported attribute"))
    } else {
        Ok(())
    }
}

pub struct FieldAttrs<'a> {
    field: &'a syn::Field,
    rename: Option<String>,
    aliases: Vec<String>,
    default: Option<TypeDefault>,
    flatten: bool,
    skip_serializing_if: Option<syn::ExprPath>,
}

impl<'a> FieldAttrs<'a> {
    pub fn of(field: &'a syn::Field) -> syn::Result<FieldAttrs<'a>> {
        let mut rv = FieldAttrs {
            field,
            rename: None,
            aliases: Vec::new(),
            default: None,
            flatten: false,
            skip_serializing_if: None,
        };

        for meta_item in field.attrs.iter().flat_map(get_meta_items).flatten() {
            if let syn::NestedMeta::Meta(meta) = meta_item {
                match &meta {
                    syn::Meta::NameValue(nv) if nv.path.is_ident("rename") => {
                        if rv.rename.is_some() {
                            return Err(syn::Error::new_spanned(
                                meta,
                                "duplicate rename attribute",
                            ));
                        }
                        rv.rename = Some(get_lit_str("rename", &nv.lit)?);
                    }
                    syn::Meta::NameValue(nv) if nv.path.is_ident("alias") => {
                        rv.aliases.push(get_lit_str("alias", &nv.lit)?);
                    }
                    syn::Meta::Path(path) if path.is_ident("default") => {
                        if rv.default.is_some() {
                            return Err(syn::Error::new_spanned(
                                meta,
                                "duplicate default attribute",
                            ));
                        }
                        rv.default = Some(TypeDefault::Implicit);
                    }
                    syn::Meta::NameValue(nv) if nv.path.is_ident("default") => {
                        if rv.default.is_some() {
                            return Err(syn::Error::new_spanned(
                                meta,
                                "duplicate default attribute",
                            ));
                        }
                        rv.default = Some(TypeDefault::Explicit(parse_lit_into_expr_path(
                            "default", &nv.lit,
                        )?));
                    }
                    syn::Meta::Path(path) if path.is_ident("flatten") => {
                        if rv.flatten {
                            return Err(syn::Error::new_spanned(
                                meta,
                                "duplicate flatten attribute",
                            ));
                        }
                        rv.flatten = true;
                    }
                    syn::Meta::NameValue(nv) if nv.path.is_ident("skip_serializing_if") => {
                        if rv.skip_serializing_if.is_some() {
                            return Err(syn::Error::new_spanned(
                                meta,
                                "duplicate skip_serializing_if attribute",
                            ));
                        }
                        rv.skip_serializing_if =
                            Some(parse_lit_into_expr_path("skip_serializing_if", &nv.lit)?);
                    }
                    _ => return Err(syn::Error::new_spanned(meta, "unsupported attribute")),
                }
            } else {
                return Err(syn::Error::new_spanned(meta_item, "unsupported attribute"));
            }
        }

        if rv.flatten && rv.default.is_some() {
            return Err(syn::Error::new_spanned(
                field,
                "cannot combine flatten and default",
            ));
        }

        Ok(rv)
    }

    pub fn field(&self) -> &syn::Field {
        self.field
    }

    pub fn name(&self, container_attrs: &ContainerAttrs) -> Cow<'_, str> {
        self.rename
            .as_deref()
            .map(Cow::Borrowed)
            .unwrap_or_else(|| container_attrs.get_field_name(self.field).into())
    }

    pub fn aliases(&self) -> &[String] {
        &self.aliases
    }

    pub fn default(&self) -> Option<&TypeDefault> {
        self.default.as_ref()
    }

    pub fn flatten(&self) -> bool {
        self.flatten
    }

    pub fn skip_serializing_if(&self) -> Option<&syn::ExprPath> {
        self.skip_serializing_if.as_ref()
    }
}

pub struct EnumVariantAttrs<'a> {
    variant: &'a syn::Variant,
    rename: Option<String>,
    aliases: Vec<String>,
}

impl<'a> EnumVariantAttrs<'a> {
    pub fn of(variant: &'a syn::Variant) -> syn::Result<EnumVariantAttrs<'a>> {
        let mut rv = EnumVariantAttrs {
            variant,
            rename: None,
            aliases: Vec::new(),
        };

        for meta_item in variant.attrs.iter().flat_map(get_meta_items).flatten() {
            if let syn::NestedMeta::Meta(meta) = meta_item {
                match &meta {
                    syn::Meta::NameValue(nv) if nv.path.is_ident("rename") => {
                        if rv.rename.is_some() {
                            return Err(syn::Error::new_spanned(
                                meta,
                                "duplicate rename attribute",
                            ));
                        }
                        rv.rename = Some(get_lit_str("rename", &nv.lit)?);
                    }
                    syn::Meta::NameValue(nv) if nv.path.is_ident("alias") => {
                        rv.aliases.push(get_lit_str("alias", &nv.lit)?);
                    }
                    _ => return Err(syn::Error::new_spanned(meta, "unsupported attribute")),
                }
            } else {
                return Err(syn::Error::new_spanned(meta_item, "unsupported attribute"));
            }
        }

        Ok(rv)
    }

    pub fn variant(&self) -> &syn::Variant {
        self.variant
    }

    pub fn name(&self, container_attrs: &ContainerAttrs) -> Cow<'_, str> {
        self.rename
            .as_deref()
            .map(Cow::Borrowed)
            .unwrap_or_else(|| container_attrs.get_variant_name(self.variant).into())
    }

    pub fn aliases(&self) -> &[String] {
        &self.aliases
    }
}