automorph-derive 0.2.0

Derive macros for automorph - Automerge-Rust struct synchronization
Documentation
//! Attribute parsing for the Automorph derive macro.

use syn::spanned::Spanned;

use crate::case_conversion::RenameRule;

/// Container-level attributes for structs and enums.
#[derive(Default)]
pub(crate) struct ContainerAttrs {
    pub(crate) rename_all: Option<RenameRule>,
    pub(crate) tag: Option<String>,
    pub(crate) content: Option<String>,
    pub(crate) untagged: bool,
    /// If true, error when document contains fields not in the struct.
    pub(crate) deny_unknown_fields: bool,
    /// If true, serialize the inner value directly (for newtype structs).
    pub(crate) transparent: bool,
}

/// Field-level attributes.
#[derive(Default)]
pub(crate) struct FieldAttrs {
    pub(crate) rename: Option<String>,
    pub(crate) skip: bool,
    pub(crate) default: bool,
    /// Custom default function path (e.g., "my_module::default_value").
    pub(crate) default_fn: Option<String>,
    /// Custom module path for save/load (e.g., "automorph::chrono_iso8601").
    pub(crate) with: Option<String>,
    /// Predicate function to skip saving (e.g., "Option::is_none").
    pub(crate) skip_saving_if: Option<String>,
    /// Flatten this field's fields into the parent struct.
    pub(crate) flatten: bool,
    /// Serialize this newtype field's inner value directly.
    pub(crate) transparent: bool,
    /// Alias for this field (e.g., old name for migration support).
    pub(crate) alias: Option<String>,
}

/// Variant-level attributes for enums.
#[derive(Default)]
pub(crate) struct VariantAttrs {
    pub(crate) rename: Option<String>,
    pub(crate) skip: bool,
}

/// Known container-level attributes
pub(crate) const KNOWN_CONTAINER_ATTRS: &[&str] = &[
    "rename_all",
    "tag",
    "content",
    "untagged",
    "deny_unknown_fields",
    "transparent",
];

/// Known field-level attributes
pub(crate) const KNOWN_FIELD_ATTRS: &[&str] = &[
    "rename",
    "skip",
    "default",
    "with",
    "skip_saving_if",
    "flatten",
    "transparent",
    "alias",
];

/// Known variant-level attributes
pub(crate) const KNOWN_VARIANT_ATTRS: &[&str] = &["rename", "skip"];

/// Validate that all automorph attributes are known.
/// Returns an error if an unknown attribute is found (likely a typo).
pub(crate) fn validate_container_attrs(attrs: &[syn::Attribute]) -> Option<syn::Error> {
    for attr in attrs {
        if !attr.path().is_ident("automorph") {
            continue;
        }

        let mut error: Option<syn::Error> = None;
        let _ = attr.parse_nested_meta(|meta| {
            let attr_name = meta.path.get_ident().map(|i| i.to_string());
            if let Some(name) = attr_name {
                if !KNOWN_CONTAINER_ATTRS.contains(&name.as_str()) {
                    error = Some(syn::Error::new(
                        meta.path.span(),
                        format!(
                            "unknown automorph container attribute `{}`. \
                            Known attributes: {}",
                            name,
                            KNOWN_CONTAINER_ATTRS.join(", ")
                        ),
                    ));
                }
            }
            // Consume any value to avoid parse errors
            if meta.input.peek(syn::Token![=]) {
                let _ = meta.value().ok().and_then(|v| v.parse::<syn::Lit>().ok());
            }
            Ok(())
        });
        if let Some(e) = error {
            return Some(e);
        }
    }
    None
}

/// Validate that all automorph field attributes are known.
pub(crate) fn validate_field_attrs(attrs: &[syn::Attribute]) -> Option<syn::Error> {
    for attr in attrs {
        if !attr.path().is_ident("automorph") {
            continue;
        }

        let mut error: Option<syn::Error> = None;
        let _ = attr.parse_nested_meta(|meta| {
            let attr_name = meta.path.get_ident().map(|i| i.to_string());
            if let Some(ref name) = attr_name {
                if !KNOWN_FIELD_ATTRS.contains(&name.as_str()) {
                    error = Some(syn::Error::new(
                        meta.path.span(),
                        format!(
                            "unknown automorph field attribute `{}`. \
                            Known attributes: {}",
                            name,
                            KNOWN_FIELD_ATTRS.join(", ")
                        ),
                    ));
                }
                // flatten is now implemented
                if name == "flatten" {
                    // No error - flatten is supported
                }
            }
            // Consume any value to avoid parse errors
            if meta.input.peek(syn::Token![=]) {
                let _ = meta.value().ok().and_then(|v| v.parse::<syn::Lit>().ok());
            }
            Ok(())
        });
        if let Some(e) = error {
            return Some(e);
        }
    }
    None
}

/// Validate that all automorph variant attributes are known.
pub(crate) fn validate_variant_attrs(attrs: &[syn::Attribute]) -> Option<syn::Error> {
    for attr in attrs {
        if !attr.path().is_ident("automorph") {
            continue;
        }

        let mut error: Option<syn::Error> = None;
        let _ = attr.parse_nested_meta(|meta| {
            let attr_name = meta.path.get_ident().map(|i| i.to_string());
            if let Some(name) = attr_name {
                if !KNOWN_VARIANT_ATTRS.contains(&name.as_str()) {
                    error = Some(syn::Error::new(
                        meta.path.span(),
                        format!(
                            "unknown automorph variant attribute `{}`. \
                            Known attributes: {}",
                            name,
                            KNOWN_VARIANT_ATTRS.join(", ")
                        ),
                    ));
                }
            }
            // Consume any value to avoid parse errors
            if meta.input.peek(syn::Token![=]) {
                let _ = meta.value().ok().and_then(|v| v.parse::<syn::Lit>().ok());
            }
            Ok(())
        });
        if let Some(e) = error {
            return Some(e);
        }
    }
    None
}

/// Parse container-level attributes from a list of attributes.
pub(crate) fn parse_container_attrs(attrs: &[syn::Attribute]) -> ContainerAttrs {
    let mut result = ContainerAttrs::default();

    for attr in attrs {
        if !attr.path().is_ident("automorph") {
            continue;
        }

        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("rename_all") {
                let value: syn::LitStr = meta.value()?.parse()?;
                result.rename_all = match value.value().as_str() {
                    "camelCase" => Some(RenameRule::CamelCase),
                    "snake_case" => Some(RenameRule::SnakeCase),
                    "PascalCase" => Some(RenameRule::PascalCase),
                    "SCREAMING_SNAKE_CASE" => Some(RenameRule::ScreamingSnakeCase),
                    "kebab-case" => Some(RenameRule::KebabCase),
                    _ => None,
                };
            } else if meta.path.is_ident("tag") {
                let value: syn::LitStr = meta.value()?.parse()?;
                result.tag = Some(value.value());
            } else if meta.path.is_ident("content") {
                let value: syn::LitStr = meta.value()?.parse()?;
                result.content = Some(value.value());
            } else if meta.path.is_ident("untagged") {
                result.untagged = true;
            } else if meta.path.is_ident("deny_unknown_fields") {
                result.deny_unknown_fields = true;
            } else if meta.path.is_ident("transparent") {
                result.transparent = true;
            }
            Ok(())
        });
    }

    result
}

/// Parse field-level attributes from a list of attributes.
pub(crate) fn parse_field_attrs(attrs: &[syn::Attribute]) -> FieldAttrs {
    let mut result = FieldAttrs::default();

    for attr in attrs {
        if !attr.path().is_ident("automorph") {
            continue;
        }

        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("rename") {
                let value: syn::LitStr = meta.value()?.parse()?;
                result.rename = Some(value.value());
            } else if meta.path.is_ident("skip") {
                result.skip = true;
            } else if meta.path.is_ident("default") {
                result.default = true;
                // Check if it's `default` or `default = "..."`
                if meta.input.peek(syn::Token![=]) {
                    let value: syn::LitStr = meta.value()?.parse()?;
                    result.default_fn = Some(value.value());
                }
            } else if meta.path.is_ident("with") {
                let value: syn::LitStr = meta.value()?.parse()?;
                result.with = Some(value.value());
            } else if meta.path.is_ident("skip_saving_if") {
                let value: syn::LitStr = meta.value()?.parse()?;
                result.skip_saving_if = Some(value.value());
            } else if meta.path.is_ident("flatten") {
                result.flatten = true;
            } else if meta.path.is_ident("transparent") {
                result.transparent = true;
            } else if meta.path.is_ident("alias") {
                let value: syn::LitStr = meta.value()?.parse()?;
                result.alias = Some(value.value());
            }
            Ok(())
        });
    }

    result
}

/// Parse variant-level attributes from a list of attributes.
pub(crate) fn parse_variant_attrs(attrs: &[syn::Attribute]) -> VariantAttrs {
    let mut result = VariantAttrs::default();

    for attr in attrs {
        if !attr.path().is_ident("automorph") {
            continue;
        }

        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("rename") {
                let value: syn::LitStr = meta.value()?.parse()?;
                result.rename = Some(value.value());
            } else if meta.path.is_ident("skip") {
                result.skip = true;
            }
            Ok(())
        });
    }

    result
}

/// Get the key (name in Automerge document) for a field.
pub(crate) fn get_field_key(field: &syn::Field, container_attrs: &ContainerAttrs) -> String {
    let field_attrs = parse_field_attrs(&field.attrs);

    if let Some(rename) = field_attrs.rename {
        return rename;
    }

    let name = field
        .ident
        .as_ref()
        .map(|i| i.to_string())
        .unwrap_or_default();

    if let Some(rule) = &container_attrs.rename_all {
        rule.apply(&name)
    } else {
        name
    }
}

/// Get the key (name in Automerge document) for a variant.
pub(crate) fn get_variant_key(variant: &syn::Variant, container_attrs: &ContainerAttrs) -> String {
    let variant_attrs = parse_variant_attrs(&variant.attrs);

    if let Some(rename) = variant_attrs.rename {
        return rename;
    }

    let name = variant.ident.to_string();

    if let Some(rule) = &container_attrs.rename_all {
        rule.apply(&name)
    } else {
        name
    }
}