1use darling::FromMeta;
2use quote::quote;
3use syn::{
4 DeriveInput, Error, GenericArgument, Ident, Path, PathArguments, Type, spanned::Spanned,
5};
6
7#[derive(Clone, Copy, PartialEq, Eq)]
8enum FilterOp {
9 Eq,
10 Ne,
11 Lt,
12 Lte,
13 Gt,
14 Gte,
15 Like,
16 ILike,
17 In,
18}
19
20impl FilterOp {
21 fn method(self) -> Ident {
22 Ident::new(
23 match self {
24 FilterOp::Eq => "eq",
25 FilterOp::Ne => "ne",
26 FilterOp::Lt => "lt",
27 FilterOp::Lte => "lte",
28 FilterOp::Gt => "gt",
29 FilterOp::Gte => "gte",
30 FilterOp::Like => "like",
31 FilterOp::ILike => "ilike",
32 FilterOp::In => "in",
33 },
34 proc_macro2::Span::call_site(),
35 )
36 }
37}
38
39struct FilterAttr {
40 op: FilterOp,
41 column: Option<String>,
42 span: proc_macro2::Span,
43}
44
45#[derive(Default, FromMeta)]
46struct FilterContainer {
47 model: Option<Path>,
48}
49
50#[derive(Default, FromMeta)]
51struct FilterMeta {
52 #[darling(default)]
53 op: Option<FilterOp>,
54 #[darling(default)]
55 column: Option<String>,
56}
57
58impl FromMeta for FilterOp {
59 fn from_string(value: &str) -> darling::Result<Self> {
60 match value {
61 "eq" => Ok(Self::Eq),
62 "ne" => Ok(Self::Ne),
63 "lt" => Ok(Self::Lt),
64 "lte" => Ok(Self::Lte),
65 "gt" => Ok(Self::Gt),
66 "gte" => Ok(Self::Gte),
67 "like" => Ok(Self::Like),
68 "ilike" => Ok(Self::ILike),
69 "in" => Ok(Self::In),
70 _ => Err(darling::Error::unknown_value(value)),
71 }
72 }
73}
74
75pub fn derive_filterable(
76 input: proc_macro2::TokenStream,
77 runtime_path: proc_macro2::TokenStream,
78) -> proc_macro2::TokenStream {
79 let input = match syn::parse2::<DeriveInput>(input) {
80 Ok(input) => input,
81 Err(err) => return err.to_compile_error(),
82 };
83 derive_filterable_impl(&input, runtime_path)
84}
85
86fn derive_filterable_impl(
87 input: &DeriveInput,
88 runtime_path: proc_macro2::TokenStream,
89) -> proc_macro2::TokenStream {
90 let ident = &input.ident;
91 let crate_path = crate::runtime_path(input, runtime_path);
92 let model = match parse_model(input) {
93 Ok(model) => model,
94 Err(e) => return e.to_compile_error(),
95 };
96 let fields = match named_fields(input) {
97 Ok(fields) => fields,
98 Err(e) => return e.to_compile_error(),
99 };
100 let filter_stmts = match filter_statements(fields, &crate_path) {
101 Ok(stmts) => stmts,
102 Err(e) => return e.to_compile_error(),
103 };
104 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
105
106 quote! {
107 impl #impl_generics #crate_path::Filterable for #ident #ty_generics #where_clause {
108 type Model = #model;
109
110 fn apply_filter(
111 &self,
112 mut filter: #crate_path::FilterBuilder<Self::Model>,
113 ) -> #crate_path::FilterBuilder<Self::Model> {
114 #(#filter_stmts)*
115 filter
116 }
117 }
118 }
119}
120
121fn named_fields(
122 input: &DeriveInput,
123) -> Result<&syn::punctuated::Punctuated<syn::Field, syn::Token![,]>, Error> {
124 match &input.data {
125 syn::Data::Struct(data) => match &data.fields {
126 syn::Fields::Named(fields) => Ok(&fields.named),
127 _ => Err(Error::new_spanned(
128 &input.ident,
129 "Filterable supports named structs only",
130 )),
131 },
132 _ => Err(Error::new_spanned(
133 &input.ident,
134 "Filterable supports structs only",
135 )),
136 }
137}
138
139fn filter_statements(
140 fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>,
141 crate_path: &proc_macro2::TokenStream,
142) -> Result<Vec<proc_macro2::TokenStream>, Error> {
143 let mut stmts = Vec::new();
144 for field in fields {
145 let Some(field_ident) = field.ident.as_ref() else {
146 continue;
147 };
148 let Some(attr) = parse_filter_attr(field)? else {
149 continue;
150 };
151 let column = match attr.column {
152 Some(column) => {
153 syn::parse_str::<Ident>(&column)
154 .map_err(|_| Error::new(attr.span, "column must be a Rust field identifier"))?;
155 Ident::new(&column, attr.span)
156 }
157 None => field_ident.clone(),
158 };
159 stmts.push(filter_statement(
160 field_ident,
161 &field.ty,
162 &column,
163 attr.op,
164 crate_path,
165 )?);
166 }
167 Ok(stmts)
168}
169
170fn filter_statement(
171 field: &Ident,
172 ty: &Type,
173 column: &Ident,
174 op: FilterOp,
175 crate_path: &proc_macro2::TokenStream,
176) -> Result<proc_macro2::TokenStream, Error> {
177 if op == FilterOp::In {
178 return in_filter_statement(field, ty, column, crate_path);
179 }
180 let method = op.method();
181 if option_inner_type(ty).is_some() {
182 return Ok(quote! {
183 if let Some(value) = &self.#field {
184 let predicate = filter.#column.#method(#crate_path::val(value.clone()));
185 filter = filter.filter(predicate);
186 }
187 });
188 }
189 Ok(quote! {
190 let predicate = filter.#column.#method(#crate_path::val(self.#field.clone()));
191 filter = filter.filter(predicate);
192 })
193}
194
195fn in_filter_statement(
196 field: &Ident,
197 ty: &Type,
198 column: &Ident,
199 crate_path: &proc_macro2::TokenStream,
200) -> Result<proc_macro2::TokenStream, Error> {
201 if let Some(inner) = option_inner_type(ty) {
202 if vec_inner_type(inner).is_none() {
203 return Err(Error::new(
204 ty.span(),
205 "the in filter operator requires Vec<T> or Option<Vec<T>>",
206 ));
207 }
208 return Ok(quote! {
209 if let Some(values) = &self.#field {
210 if !values.is_empty() {
211 let predicate = #crate_path::filters::__private::in_values(
212 &filter.#column,
213 values.iter().cloned().map(#crate_path::val),
214 );
215 filter = filter.filter(predicate);
216 }
217 }
218 });
219 }
220 if vec_inner_type(ty).is_none() {
221 return Err(Error::new(
222 ty.span(),
223 "the in filter operator requires Vec<T> or Option<Vec<T>>",
224 ));
225 }
226 Ok(quote! {
227 if !self.#field.is_empty() {
228 let predicate = #crate_path::filters::__private::in_values(
229 &filter.#column,
230 self.#field.iter().cloned().map(#crate_path::val),
231 );
232 filter = filter.filter(predicate);
233 }
234 })
235}
236
237fn parse_model(input: &DeriveInput) -> Result<Path, Error> {
238 let mut model = None;
239 for attr in input
240 .attrs
241 .iter()
242 .filter(|attr| attr.path().is_ident("filter"))
243 {
244 let nested = parse_nested(attr)?;
245 let parsed = FilterContainer::from_list(&nested)
246 .map_err(|err| Error::new(attr.span(), err.to_string()))?;
247 if model.is_some() && parsed.model.is_some() {
248 return Err(Error::new(attr.span(), "model can only be set once"));
249 }
250 model = model.or(parsed.model);
251 }
252 model.ok_or_else(|| {
253 Error::new_spanned(
254 &input.ident,
255 "Filterable requires #[filter(model = ModelType)]",
256 )
257 })
258}
259
260fn parse_filter_attr(field: &syn::Field) -> Result<Option<FilterAttr>, Error> {
261 let attrs: Vec<_> = field
262 .attrs
263 .iter()
264 .filter(|attr| attr.path().is_ident("filter"))
265 .collect();
266 if attrs.is_empty() {
267 return Ok(None);
268 }
269 if attrs.len() > 1 {
270 return Err(Error::new(
271 attrs[1].span(),
272 "only one #[filter(...)] attribute is supported per field",
273 ));
274 }
275 let nested = parse_nested(attrs[0])?;
276 let parsed = FilterMeta::from_list(&nested)
277 .map_err(|err| Error::new(attrs[0].span(), err.to_string()))?;
278 Ok(Some(FilterAttr {
279 op: parsed.op.unwrap_or(FilterOp::Eq),
280 column: parsed.column,
281 span: attrs[0].span(),
282 }))
283}
284
285fn parse_nested(attr: &syn::Attribute) -> Result<Vec<darling::ast::NestedMeta>, Error> {
286 match &attr.meta {
287 syn::Meta::List(list) => darling::ast::NestedMeta::parse_meta_list(list.tokens.clone())
288 .map_err(|err| Error::new(attr.span(), err.to_string())),
289 syn::Meta::Path(_) => Ok(Vec::new()),
290 _ => Err(Error::new(attr.span(), "expected #[filter(...)]")),
291 }
292}
293
294fn option_inner_type(ty: &Type) -> Option<&Type> {
295 let Type::Path(path) = ty else {
296 return None;
297 };
298 let segment = path.path.segments.last()?;
299 if segment.ident != "Option" {
300 return None;
301 }
302 let PathArguments::AngleBracketed(args) = &segment.arguments else {
303 return None;
304 };
305 let Some(GenericArgument::Type(inner)) = args.args.first() else {
306 return None;
307 };
308 Some(inner)
309}
310
311fn vec_inner_type(ty: &Type) -> Option<&Type> {
312 let Type::Path(path) = ty else {
313 return None;
314 };
315 let segment = path.path.segments.last()?;
316 if segment.ident != "Vec" {
317 return None;
318 }
319 let PathArguments::AngleBracketed(args) = &segment.arguments else {
320 return None;
321 };
322 let Some(GenericArgument::Type(inner)) = args.args.first() else {
323 return None;
324 };
325 Some(inner)
326}