deepmerge-derive 0.1.0

Derive macro for the DeepMerge trait with typed attribute support
Documentation
//! Parsing module for derive macro attributes
//!
//! This module handles parsing of the #[merge(...)] attributes and converting them
//! into structured data that can be validated and used for code generation.

use darling::{ast, FromDeriveInput, FromField};
use syn::Attribute;
use quote::ToTokens;

/// Represents a parsed typed policy value that can be a literal, identifier, or path
#[derive(Debug, Clone)]
pub enum TypedPolicyValue {
    /// String literal value like "concat"
    Literal(String),
    /// Identifier like `append`
    Ident(syn::Ident),
    /// Path expression like `StringMerge::ConcatWithSep(", ")`
    Path(syn::Expr),
}

impl TypedPolicyValue {
    /// Convert to a string if this is a literal
    pub fn as_literal_str(&self) -> Option<&str> {
        match self {
            TypedPolicyValue::Literal(s) => Some(s),
            _ => None,
        }
    }
}

/// Parsed condition policy configuration
#[derive(Debug, Clone, Default)]
pub struct ConditionPolicy {
    pub condition: Option<TypedPolicyValue>,
    pub changed_by: Option<syn::Expr>,
}

/// Parsed struct-level policy configuration
#[derive(Debug, Clone, Default)]
pub struct StructPolicy {
    pub sequence: Option<TypedPolicyValue>,
    pub map: Option<TypedPolicyValue>,
    pub option: Option<TypedPolicyValue>,
    pub number: Option<TypedPolicyValue>,
    pub bool_policy: Option<TypedPolicyValue>,
    pub string: Option<TypedPolicyValue>,
    pub condition: Option<ConditionPolicy>,
    pub sequence_dedupe: Option<bool>,
    pub unique_by: Option<syn::Expr>,
}

/// Consolidated field configuration from attributes
#[derive(Debug, Clone, Default)]
pub struct FieldConfig {
    pub skip: bool,
    pub with: Option<syn::Path>,
    pub sequence: Option<TypedPolicyValue>,
    pub map: Option<TypedPolicyValue>,
    pub bool_config: Option<TypedPolicyValue>,
    pub number: Option<TypedPolicyValue>,
    pub option: Option<TypedPolicyValue>,
    pub string: Option<TypedPolicyValue>,
    pub condition: Option<TypedPolicyValue>,
    pub dedupe: bool,
    pub unique_by: Option<syn::Expr>,
}

/// Parse field configuration from raw merge attributes
fn parse_field_config_from_attr(attr: &Attribute) -> syn::Result<FieldConfig> {
    let mut config = FieldConfig::default();
    
    attr.parse_nested_meta(|nested| {
        if nested.path.is_ident("skip") {
            config.skip = true;
            Ok(())
        } else if nested.path.is_ident("dedupe") {
            // Flag attribute: #[merge(dedupe)]
            config.dedupe = true;
            Ok(())
        } else if nested.path.is_ident("unique_by") {
            // Value attribute: #[merge(unique_by = path::to::key_fn)]
            let value = nested.value()?;
            let expr: syn::Expr = value.parse()?;
            config.unique_by = Some(expr);
            Ok(())
        } else if nested.path.is_ident("with") {
            let value = nested.value()?;
            let path_str: syn::LitStr = value.parse()?;
            config.with = Some(syn::parse_str(&path_str.value())?);
            Ok(())
        } else if nested.path.is_ident("sequence") {
            let value = parse_policy_value(&nested)?;
            config.sequence = Some(value);
            Ok(())
        } else if nested.path.is_ident("map") {
            let value = parse_policy_value(&nested)?;
            config.map = Some(value);
            Ok(())
        } else if nested.path.is_ident("bool") {
            let value = parse_policy_value(&nested)?;
            config.bool_config = Some(value);
            Ok(())
        } else if nested.path.is_ident("number") {
            let value = parse_policy_value(&nested)?;
            config.number = Some(value);
            Ok(())
        } else if nested.path.is_ident("option") {
            let value = parse_policy_value(&nested)?;
            config.option = Some(value);
            Ok(())
        } else if nested.path.is_ident("string") {
            let value = parse_policy_value(&nested)?;
            config.string = Some(value);
            Ok(())
        } else if nested.path.is_ident("condition") {
            let value = parse_policy_value(&nested)?;
            config.condition = Some(value);
            Ok(())
        } else {
            Ok(()) // Ignore unknown attributes for now
        }
    })?;
    
    Ok(config)
}

/// Parse a policy value from nested meta
fn parse_policy_value(nested: &syn::meta::ParseNestedMeta) -> syn::Result<TypedPolicyValue> {
    // First check if there's an equals sign (value assignment)
    if nested.input.peek(syn::Token![=]) {
        let value = nested.value()?;
        
        // Try to parse as a string literal first
        if let Ok(lit_str) = value.parse::<syn::LitStr>() {
            return Ok(TypedPolicyValue::Literal(lit_str.value()));
        }
        
        // Try to parse as an identifier (like `string = keep`)
        if let Ok(ident) = value.parse::<syn::Ident>() {
            return Ok(TypedPolicyValue::Ident(ident));
        }
        
        // Otherwise parse as an expression
        let expr: syn::Expr = value.parse()?;
        Ok(TypedPolicyValue::Path(expr))
    } else {
        // No equals sign means just the attribute name, which might be valid for some attrs
        Err(nested.error("Expected = after policy type"))
    }
}

/// Parsed field with merge attributes
#[derive(Debug, Clone, FromField)]
#[darling(forward_attrs(merge))]
pub struct MergeField {
    pub ident: Option<syn::Ident>,
    pub ty: syn::Type,
    
    // Access to raw attributes for manual parsing
    pub attrs: Vec<Attribute>,
}

/// Main input structure for the derive macro
#[derive(Debug, FromDeriveInput)]
#[darling(supports(struct_named, enum_any), forward_attrs(allow))]
pub struct MergeInput {
    pub ident: syn::Ident,
    pub generics: syn::Generics,
    pub data: ast::Data<(), MergeField>,
    pub attrs: Vec<Attribute>,
}

/// Parse struct-level policy from attributes
pub fn parse_struct_policy(attrs: &[Attribute]) -> darling::Result<Option<StructPolicy>> {
    for attr in attrs {
        if attr.path().is_ident("merge") {
            let mut struct_policy = StructPolicy::default();
            
            attr.parse_nested_meta(|nested| {
                if nested.path.is_ident("policy") {
                    nested.parse_nested_meta(|policy_nested| {
                        assign_to_struct_policy(&mut struct_policy, &policy_nested)
                    })?;
                    Ok(())
                } else if nested.path.is_ident("sequence_dedupe") {
                    let value = nested.value()?;
                    struct_policy.sequence_dedupe = Some(value.parse::<syn::LitBool>()?.value());
                    Ok(())
                } else if nested.path.is_ident("unique_by") {
                    let value = nested.value()?;
                    struct_policy.unique_by = Some(value.parse()?);
                    Ok(())
                } else {
                    Ok(()) // Ignore unknown for now
                }
            })?;
            
            return Ok(Some(struct_policy));
        }
    }
    Ok(None)
}

/// Assign a policy value to the appropriate field in `StructPolicy`
fn assign_to_struct_policy(
    struct_policy: &mut StructPolicy,
    nested: &syn::meta::ParseNestedMeta,
) -> syn::Result<()> {
    let key = nested.path.to_token_stream().to_string();
    let value = parse_policy_value(nested)?;
    
    match key.as_str() {
        "sequence" => struct_policy.sequence = Some(value),
        "map" => struct_policy.map = Some(value),
        "option" => struct_policy.option = Some(value),
        "number" => struct_policy.number = Some(value),
        "bool" => struct_policy.bool_policy = Some(value),
        "string" => struct_policy.string = Some(value),
        "condition" => {
            if struct_policy.condition.is_none() {
                struct_policy.condition = Some(ConditionPolicy::default());
            }
            struct_policy.condition.as_mut().unwrap().condition = Some(value);
        }
        "changed_by" => {
            if struct_policy.condition.is_none() {
                struct_policy.condition = Some(ConditionPolicy::default());
            }
            let expr = nested.value()?.parse()?;
            struct_policy.condition.as_mut().unwrap().changed_by = Some(expr);
        }
        _ => {
            return Err(nested.error(format!("Unknown policy type: {key}")));
        }
    }
    
    Ok(())
}

/// Parse field-level typed policy values from attributes
pub fn parse_field_typed_policies(field: &MergeField) -> FieldConfig {
    let mut config = FieldConfig::default();
    
    // Parse typed attributes from raw attrs
    for attr in &field.attrs {
        if attr.path().is_ident("merge")
            && let Ok(field_config) = parse_field_config_from_attr(attr) {
            // Merge with existing config, later values take precedence
            if field_config.skip {
                config.skip = true;
            }
            if field_config.dedupe {
                config.dedupe = true;
            }
            if field_config.unique_by.is_some() {
                config.unique_by = field_config.unique_by;
            }
            if field_config.with.is_some() {
                config.with = field_config.with;
            }
            if field_config.sequence.is_some() {
                config.sequence = field_config.sequence;
            }
            if field_config.map.is_some() {
                config.map = field_config.map;
            }
            if field_config.bool_config.is_some() {
                config.bool_config = field_config.bool_config;
            }
            if field_config.number.is_some() {
                config.number = field_config.number;
            }
            if field_config.option.is_some() {
                config.option = field_config.option;
            }
            if field_config.string.is_some() {
                config.string = field_config.string;
            }
            if field_config.condition.is_some() {
                config.condition = field_config.condition;
            }
        }
    }
    
    config
}