use darling::{ast, FromDeriveInput, FromField};
use syn::Attribute;
use quote::ToTokens;
#[derive(Debug, Clone)]
pub enum TypedPolicyValue {
Literal(String),
Ident(syn::Ident),
Path(syn::Expr),
}
impl TypedPolicyValue {
pub fn as_literal_str(&self) -> Option<&str> {
match self {
TypedPolicyValue::Literal(s) => Some(s),
_ => None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ConditionPolicy {
pub condition: Option<TypedPolicyValue>,
pub changed_by: Option<syn::Expr>,
}
#[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>,
}
#[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>,
}
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") {
config.dedupe = true;
Ok(())
} else if nested.path.is_ident("unique_by") {
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(()) }
})?;
Ok(config)
}
fn parse_policy_value(nested: &syn::meta::ParseNestedMeta) -> syn::Result<TypedPolicyValue> {
if nested.input.peek(syn::Token![=]) {
let value = nested.value()?;
if let Ok(lit_str) = value.parse::<syn::LitStr>() {
return Ok(TypedPolicyValue::Literal(lit_str.value()));
}
if let Ok(ident) = value.parse::<syn::Ident>() {
return Ok(TypedPolicyValue::Ident(ident));
}
let expr: syn::Expr = value.parse()?;
Ok(TypedPolicyValue::Path(expr))
} else {
Err(nested.error("Expected = after policy type"))
}
}
#[derive(Debug, Clone, FromField)]
#[darling(forward_attrs(merge))]
pub struct MergeField {
pub ident: Option<syn::Ident>,
pub ty: syn::Type,
pub attrs: Vec<Attribute>,
}
#[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>,
}
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(()) }
})?;
return Ok(Some(struct_policy));
}
}
Ok(None)
}
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(())
}
pub fn parse_field_typed_policies(field: &MergeField) -> FieldConfig {
let mut config = FieldConfig::default();
for attr in &field.attrs {
if attr.path().is_ident("merge")
&& let Ok(field_config) = parse_field_config_from_attr(attr) {
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
}