condition_matcher_derive/
lib.rs

1//! # Condition Matcher Derive
2//!
3//! This crate provides the `#[derive(Matchable)]` procedural macro for the `condition-matcher` crate.
4//!
5//! ## Usage
6//!
7//! ```rust,ignore
8//! use condition_matcher::{Matchable, MatchableDerive};
9//!
10//! #[derive(MatchableDerive, PartialEq)]
11//! struct User {
12//!     name: String,
13//!     age: u32,
14//!     email: Option<String>,
15//! }
16//! ```
17
18use proc_macro::TokenStream;
19use quote::quote;
20use syn::{parse_macro_input, Data, DeriveInput, Fields, Type};
21
22/// Derive macro for implementing the `Matchable` trait.
23///
24/// This macro automatically implements the `get_field` method for structs,
25/// allowing field-based condition matching.
26///
27/// ## Example
28///
29/// ```rust,ignore
30/// #[derive(Matchable, PartialEq)]
31/// struct Product {
32///     id: i32,
33///     name: String,
34///     price: f64,
35///     in_stock: bool,
36///     description: Option<String>,
37/// }
38/// ```
39///
40/// The macro generates:
41/// - `get_field(&self, field: &str) -> Option<&dyn Any>` - Returns a reference to any field by name
42/// - Handles `Option<T>` fields by unwrapping them when present
43#[proc_macro_derive(Matchable)]
44pub fn matchable_derive(input: TokenStream) -> TokenStream {
45    let input = parse_macro_input!(input as DeriveInput);
46    let name = &input.ident;
47    let generics = &input.generics;
48    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
49
50    let field_match_arms = match &input.data {
51        Data::Struct(data) => match &data.fields {
52            Fields::Named(fields) => {
53                let arms = fields.named.iter().map(|f| {
54                    let field_name = &f.ident;
55                    let field_name_str = field_name.as_ref().unwrap().to_string();
56                    let field_type = &f.ty;
57                    
58                    // Check if the field is an Option type
59                    if is_option_type(field_type) {
60                        quote! {
61                            #field_name_str => self.#field_name.as_ref().map(|v| v as &dyn std::any::Any),
62                        }
63                    } else {
64                        quote! {
65                            #field_name_str => Some(&self.#field_name as &dyn std::any::Any),
66                        }
67                    }
68                });
69                quote! {
70                    #(#arms)*
71                }
72            }
73            Fields::Unnamed(_) => {
74                // For tuple structs, use indices
75                quote! {}
76            }
77            Fields::Unit => {
78                quote! {}
79            }
80        },
81        Data::Enum(_) => {
82            quote! {}
83        }
84        Data::Union(_) => {
85            quote! {}
86        }
87    };
88
89    // Generate is_none implementation for types with Option fields
90    let has_option_fields = match &input.data {
91        Data::Struct(data) => match &data.fields {
92            Fields::Named(fields) => fields.named.iter().any(|f| is_option_type(&f.ty)),
93            _ => false,
94        },
95        _ => false,
96    };
97
98    let is_none_impl = if has_option_fields {
99        quote! {
100            fn is_none(&self) -> bool {
101                false
102            }
103        }
104    } else {
105        quote! {}
106    };
107
108    // Generate length implementation if the struct has a "len" field or method
109    let length_impl = match &input.data {
110        Data::Struct(data) => match &data.fields {
111            Fields::Named(fields) => {
112                let has_len_field = fields.named.iter().any(|f| {
113                    f.ident.as_ref().map(|i| i == "len" || i == "length").unwrap_or(false)
114                });
115                if has_len_field {
116                    quote! {
117                        fn get_length(&self) -> Option<usize> {
118                            Some(self.len as usize)
119                        }
120                    }
121                } else {
122                    quote! {}
123                }
124            }
125            _ => quote! {},
126        },
127        _ => quote! {},
128    };
129
130    let expanded = quote! {
131        impl #impl_generics Matchable for #name #ty_generics #where_clause {
132            fn get_field(&self, field: &str) -> Option<&dyn std::any::Any> {
133                match field {
134                    #field_match_arms
135                    _ => None,
136                }
137            }
138            
139            #length_impl
140            #is_none_impl
141        }
142    };
143
144    TokenStream::from(expanded)
145}
146
147/// Check if a type is an Option<T>
148fn is_option_type(ty: &Type) -> bool {
149    if let Type::Path(type_path) = ty
150        && let Some(segment) = type_path.path.segments.last()
151    {
152        return segment.ident == "Option";
153    }
154    false
155}