deepmerge-derive 0.1.0

Derive macro for the DeepMerge trait with typed attribute support
Documentation
//! Validation module for derive macro attributes
//!
//! This module handles validation of parsed attributes, including:
//! - Policy constraint checking
//! - Type compatibility validation
//! - Diagnostic generation with helpful error messages

use std::collections::HashMap;
use darling::Error as DarlingError;
use syn::Type;

use crate::parse::{StructPolicy, FieldConfig, MergeField};

/// Policy constraint requirement for a specific type
#[derive(Debug, Clone)]
pub struct PolicyConstraint {
    /// The trait bounds required (e.g., "Eq + Hash")
    pub bounds: Vec<String>,
    /// Human-readable description of why these bounds are needed
    pub reason: String,
}

/// Constraint analyzer for policy requirements
pub struct ConstraintAnalyzer {
    /// Map from policy setting to required constraints
    sequence_constraints: HashMap<String, PolicyConstraint>,
}

impl ConstraintAnalyzer {
    pub fn new() -> Self {
        let mut sequence_constraints = HashMap::new();
        
        // Sequence merge constraints
        sequence_constraints.insert(
            "union".to_string(),
            PolicyConstraint {
                bounds: vec!["Eq".to_string(), "std::hash::Hash".to_string()],
                reason: "Union operation requires elements to be comparable and hashable for deduplication".to_string(),
            }
        );
        
        sequence_constraints.insert(
            "intersect".to_string(),
            PolicyConstraint {
                bounds: vec!["Eq".to_string(), "std::hash::Hash".to_string()],
                reason: "Intersection operation requires elements to be comparable and hashable".to_string(),
            }
        );
        
        Self {
            sequence_constraints,
        }
    }
    
    pub fn check_sequence_policy(&self, policy: &str) -> Option<&PolicyConstraint> {
        self.sequence_constraints.get(policy)
    }
    
    pub fn check_map_policy(_policy: &str) -> Option<&'static PolicyConstraint> {
        // Map policies generally don't have special constraints yet
        None
    }
    
    /// Check field-level constraints and generate diagnostics
    pub fn check_field_constraints(
        &self,
        field: &MergeField,
        config: &FieldConfig,
    ) -> Vec<DarlingError> {
        let mut diagnostics = Vec::new();
        
        // Sequence constraints are documented but not enforced here; the derive injects
        // necessary bounds in the where-clause for supported cases (e.g., Vec<T> union/intersect).
        
        // Map constraints: none enforced at derive-time currently.
        
        diagnostics
    }
}

/// Get allowed values for a policy type
pub fn get_allowed_values_for_policy(policy_type: &str) -> &'static str {
    match policy_type {
        "sequence" => "append, prepend, union, extend, intersect",
        "map" => "overlay, union, left, right",
        "option" => "take, preserve, or_left",
        "number" => "sum, max, min, replace, keep",
        "bool" => "true_wins, false_wins, replace, keep",
        "string" => "concat, keep, replace, or a path expression like StringMerge::ConcatWithSep(\", \")",
        "condition" => "always, non_empty, non_default, some, changed",
        _ => "unknown policy type",
    }
}


/// Generate error message for invalid policy value
pub fn get_invalid_value_error(policy_type: &str, invalid_value: &str) -> String {
    let allowed = get_allowed_values_for_policy(policy_type);
    let suggestion = get_suggestion_for_invalid_value(policy_type, invalid_value);
    
    if suggestion.is_empty() {
        format!(
            "Invalid {policy_type} policy '{invalid_value}'. Allowed values: {allowed}"
        )
    } else {
        format!(
            "Invalid {policy_type} policy '{invalid_value}'. Did you mean '{suggestion}'? Allowed values: {allowed}"
        )
    }
}

/// Get suggestion for invalid value based on edit distance
pub fn get_suggestion_for_invalid_value(policy_type: &str, invalid_value: &str) -> String {
    let allowed_values = match policy_type {
        "sequence" => vec!["append", "prepend", "union", "extend", "intersect"],
        "map" => vec!["overlay", "union", "left", "right"],
        "option" => vec!["take", "preserve", "or_left"],
        "number" => vec!["sum", "max", "min", "replace", "keep"],
        "bool" => vec!["true_wins", "false_wins", "replace", "keep"],
        "string" => vec!["concat", "keep", "replace"],
        "condition" => vec!["always", "non_empty", "non_default", "some", "changed"],
        _ => vec![],
    };
    
    // Simple suggestion: find the closest match by prefix
    for &allowed in &allowed_values {
        if allowed.starts_with(&invalid_value.to_lowercase()) {
            return allowed.to_string();
        }
    }
    
    // If no prefix match, look for contains
    for &allowed in &allowed_values {
        if allowed.contains(&invalid_value.to_lowercase()) || 
           invalid_value.to_lowercase().contains(allowed) {
            return allowed.to_string();
        }
    }
    
    String::new()
}

/// Check if a literal value is valid for a policy type
pub fn is_valid_literal_value(policy_type: &str, value: &str) -> bool {
    match policy_type {
        "sequence" => matches!(value, "append" | "prepend" | "union" | "extend" | "intersect"),
        "map" => matches!(value, "overlay" | "union" | "left" | "right"),
        "option" => matches!(value, "take" | "preserve" | "or_left"),
        "number" => matches!(value, "sum" | "max" | "min" | "replace" | "keep"),
        "bool" => matches!(value, "true_wins" | "false_wins" | "replace" | "keep"),
        "string" => matches!(value, "concat" | "keep" | "replace"),
        "condition" => matches!(value, "always" | "non_empty" | "non_default" | "some" | "changed"),
        _ => false,
    }
}

/// Helper function to validate a policy value
fn validate_policy_value(
    policy_type: &str,
    policy_value: Option<&crate::parse::TypedPolicyValue>,
    diagnostics: &mut Vec<DarlingError>,
) {
    if let Some(policy) = policy_value
        && let Some(literal) = policy.as_literal_str()
        && !is_valid_literal_value(policy_type, literal) {
        diagnostics.push(DarlingError::custom(
            get_invalid_value_error(policy_type, literal)
        ));
    }
}

/// Helper function to validate a field policy value with span
fn validate_field_policy_value(
    policy_type: &str,
    policy_value: Option<&crate::parse::TypedPolicyValue>,
    field: &MergeField,
    diagnostics: &mut Vec<DarlingError>,
) {
    if let Some(policy) = policy_value
        && let Some(literal) = policy.as_literal_str()
        && !is_valid_literal_value(policy_type, literal) {
        let err = DarlingError::custom(
            get_invalid_value_error(policy_type, literal)
        );
        diagnostics.push(err.with_span(&field.ident));
    }
}

/// Validate struct-level policy
pub fn validate_struct_policy(policy: &StructPolicy) -> Vec<DarlingError> {
    let mut diagnostics = Vec::new();
    
    // Validate all policy types (invalid literals should trigger compile errors)
    validate_policy_value("sequence", policy.sequence.as_ref(), &mut diagnostics);
    validate_policy_value("map", policy.map.as_ref(), &mut diagnostics);
    validate_policy_value("option", policy.option.as_ref(), &mut diagnostics);
    validate_policy_value("number", policy.number.as_ref(), &mut diagnostics);
    validate_policy_value("bool", policy.bool_policy.as_ref(), &mut diagnostics);
    validate_policy_value("string", policy.string.as_ref(), &mut diagnostics);
    
    // Validate condition policy (special case due to nested structure)
    if let Some(ref condition) = policy.condition
        && let Some(ref cond) = condition.condition {
        validate_policy_value("condition", Some(cond), &mut diagnostics);
    }
    
    diagnostics
}

/// Validate field configuration
pub fn validate_field_config(
    field: &MergeField,
    config: &FieldConfig,
    analyzer: &ConstraintAnalyzer,
) -> Vec<DarlingError> {
    let mut diagnostics = Vec::new();
    
    // Check field-level constraints
    diagnostics.extend(analyzer.check_field_constraints(field, config));
    
    // Validate field policy values (preserve compile-fail behavior for invalid literals/types)
    validate_field_policy_value("sequence", config.sequence.as_ref(), field, &mut diagnostics);
    validate_field_policy_value("map", config.map.as_ref(), field, &mut diagnostics);
    validate_field_policy_value("bool", config.bool_config.as_ref(), field, &mut diagnostics);
    validate_field_policy_value("number", config.number.as_ref(), field, &mut diagnostics);
    validate_field_policy_value("option", config.option.as_ref(), field, &mut diagnostics);
    validate_field_policy_value("string", config.string.as_ref(), field, &mut diagnostics);
    validate_field_policy_value("condition", config.condition.as_ref(), field, &mut diagnostics);

    // Validate dedupe flags: only meaningful on sequences
    if (config.dedupe || config.unique_by.is_some()) && !is_sequence_type(&field.ty) {
        let field_name = field.ident.as_ref()
            .map_or_else(|| "unnamed field".to_string(), std::string::ToString::to_string);
        let msg = if config.dedupe && config.unique_by.is_some() {
            format!("Field '{field_name}': #[merge(dedupe)] and #[merge(unique_by = ...)] are only valid on sequence types like Vec<T>.")
        } else if config.dedupe {
            format!("Field '{field_name}': #[merge(dedupe)] is only valid on sequence types like Vec<T>.")
        } else {
            format!("Field '{field_name}': #[merge(unique_by = ...)] is only valid on sequence types like Vec<T>.")
        };
        diagnostics.push(DarlingError::custom(msg).with_span(&field.ty));
    }
    
    diagnostics
}

/// Check if a type is a sequence type (Vec, array, etc.)
pub fn is_sequence_type(ty: &Type) -> bool {
    match ty {
        Type::Path(type_path) => {
            if let Some(segment) = type_path.path.segments.last() {
                matches!(segment.ident.to_string().as_str(), "Vec" | "VecDeque" | "LinkedList")
            } else {
                false
            }
        }
        Type::Array(_) | Type::Slice(_) => true,
        _ => false,
    }
}

/// Check if a type is a map type (`HashMap`, `BTreeMap`, etc.)
pub fn is_map_type(ty: &Type) -> bool {
    match ty {
        Type::Path(type_path) => {
            if let Some(segment) = type_path.path.segments.last() {
                matches!(segment.ident.to_string().as_str(), "HashMap" | "BTreeMap" | "IndexMap")
            } else {
                false
            }
        }
        _ => false,
    }
}