eure-macros 0.1.9

Derive macros for Eure schema generation
Documentation
use darling::FromField;
use syn::spanned::Spanned;

use crate::attrs::{FieldAttrs, extract_eure_attr_spans};
use crate::context::MacroContext;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FieldMode {
    Record,
    Ext,
    Flatten,
    FlattenExt,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RenameScope {
    Container,
    Field,
}

#[derive(Debug, Clone)]
pub(crate) struct CommonFieldIr {
    pub ident: syn::Ident,
    pub ty: syn::Type,
    pub mode: FieldMode,
    pub wire_name: String,
}

pub(crate) fn analyze_common_named_fields(
    context: &MacroContext,
    fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
    rename_scope: RenameScope,
) -> syn::Result<Vec<CommonFieldIr>> {
    let mut out = Vec::with_capacity(fields.len());

    for field in fields {
        let ident = field.ident.clone().expect("named fields must have names");
        let ty = field.ty.clone();
        let attrs = FieldAttrs::from_field(field).expect("failed to parse field attributes");
        let attr_spans = extract_eure_attr_spans(&field.attrs);

        if attrs.flatten && attrs.flatten_ext {
            let span = attr_spans
                .get("flatten")
                .copied()
                .unwrap_or_else(|| field.span());
            return Err(syn::Error::new(
                span,
                "cannot use both #[eure(flatten)] and #[eure(flatten_ext)] on the same field",
            ));
        }
        if attrs.flatten && attrs.ext {
            let span = attr_spans
                .get("flatten")
                .copied()
                .unwrap_or_else(|| field.span());
            return Err(syn::Error::new(
                span,
                "cannot use both #[eure(flatten)] and #[eure(ext)] on the same field",
            ));
        }
        if attrs.ext && attrs.flatten_ext {
            let span = attr_spans
                .get("ext")
                .copied()
                .unwrap_or_else(|| field.span());
            return Err(syn::Error::new(
                span,
                "cannot use both #[eure(ext)] and #[eure(flatten_ext)] on the same field",
            ));
        }
        if attrs.via.is_some() && (attrs.flatten || attrs.flatten_ext) {
            let span = attr_spans
                .get("via")
                .copied()
                .unwrap_or_else(|| field.span());
            return Err(syn::Error::new(
                span,
                format!(
                    "cannot use #[eure(via = \"...\")] with #[eure(flatten)] or #[eure(flatten_ext)] on field `{}`",
                    ident
                ),
            ));
        }

        let mode = if attrs.flatten {
            FieldMode::Flatten
        } else if attrs.flatten_ext {
            FieldMode::FlattenExt
        } else if attrs.ext {
            FieldMode::Ext
        } else {
            FieldMode::Record
        };

        let wire_name = attrs.rename.clone().unwrap_or_else(|| match rename_scope {
            RenameScope::Container => context.apply_rename(&ident.to_string()),
            RenameScope::Field => context.apply_field_rename(&ident.to_string()),
        });

        out.push(CommonFieldIr {
            ident,
            ty,
            mode,
            wire_name,
        });
    }

    Ok(out)
}

#[cfg(test)]
mod tests {
    use darling::FromDeriveInput;
    use syn::parse_quote;

    use super::*;
    use crate::attrs::{ContainerAttrs, extract_container_attr_spans};
    use crate::config::MacroConfig;

    fn context_for(input: syn::DeriveInput) -> MacroContext {
        let attrs = ContainerAttrs::from_derive_input(&input).expect("valid attrs");
        let spans = extract_container_attr_spans(&input);
        let config = MacroConfig::from_attrs(attrs, spans).expect("valid config");
        MacroContext::new(config, input)
    }

    #[test]
    fn rejects_flatten_ext_conflict() {
        let input: syn::DeriveInput = parse_quote! {
            struct Sample {
                #[eure(flatten, ext)]
                inner: Inner
            }
        };
        let context = context_for(input.clone());
        let fields = match &input.data {
            syn::Data::Struct(data) => match &data.fields {
                syn::Fields::Named(fields) => &fields.named,
                _ => panic!("expected named fields"),
            },
            _ => panic!("expected struct"),
        };

        let err = analyze_common_named_fields(&context, fields, RenameScope::Container)
            .expect_err("expected conflict error");
        assert!(
            err.to_string()
                .contains("cannot use both #[eure(flatten)] and #[eure(ext)]")
        );
    }

    #[test]
    fn resolves_container_rename() {
        let input: syn::DeriveInput = parse_quote! {
            #[eure(rename_all = "kebab-case")]
            struct Sample {
                user_name: String
            }
        };
        let context = context_for(input.clone());
        let fields = match &input.data {
            syn::Data::Struct(data) => match &data.fields {
                syn::Fields::Named(fields) => &fields.named,
                _ => panic!("expected named fields"),
            },
            _ => panic!("expected struct"),
        };

        let fields = analyze_common_named_fields(&context, fields, RenameScope::Container)
            .expect("valid ir");
        assert_eq!(fields[0].wire_name, "user-name");
    }
}