use std::collections::HashMap;
use darling::Error as DarlingError;
use syn::Type;
use crate::parse::{StructPolicy, FieldConfig, MergeField};
#[derive(Debug, Clone)]
pub struct PolicyConstraint {
pub bounds: Vec<String>,
pub reason: String,
}
pub struct ConstraintAnalyzer {
sequence_constraints: HashMap<String, PolicyConstraint>,
}
impl ConstraintAnalyzer {
pub fn new() -> Self {
let mut sequence_constraints = HashMap::new();
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> {
None
}
pub fn check_field_constraints(
&self,
field: &MergeField,
config: &FieldConfig,
) -> Vec<DarlingError> {
let mut diagnostics = Vec::new();
diagnostics
}
}
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",
}
}
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}"
)
}
}
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![],
};
for &allowed in &allowed_values {
if allowed.starts_with(&invalid_value.to_lowercase()) {
return allowed.to_string();
}
}
for &allowed in &allowed_values {
if allowed.contains(&invalid_value.to_lowercase()) ||
invalid_value.to_lowercase().contains(allowed) {
return allowed.to_string();
}
}
String::new()
}
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,
}
}
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)
));
}
}
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));
}
}
pub fn validate_struct_policy(policy: &StructPolicy) -> Vec<DarlingError> {
let mut diagnostics = Vec::new();
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);
if let Some(ref condition) = policy.condition
&& let Some(ref cond) = condition.condition {
validate_policy_value("condition", Some(cond), &mut diagnostics);
}
diagnostics
}
pub fn validate_field_config(
field: &MergeField,
config: &FieldConfig,
analyzer: &ConstraintAnalyzer,
) -> Vec<DarlingError> {
let mut diagnostics = Vec::new();
diagnostics.extend(analyzer.check_field_constraints(field, config));
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);
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
}
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,
}
}
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,
}
}