prax_query/
macros.rs

1//! Compile-time filter construction macros.
2//!
3//! These macros provide zero-cost filter construction at compile time,
4//! avoiding runtime allocations for static filter patterns.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use prax_query::{filter, and_filter, or_filter};
10//! use prax_query::filter::Filter;
11//!
12//! // Simple equality filter
13//! let f = filter!(id == 42);
14//! assert!(matches!(f, Filter::Equals(_, _)));
15//!
16//! // Multiple conditions with AND
17//! let f = and_filter!(
18//!     filter!(active == true),
19//!     filter!(score > 100)
20//! );
21//!
22//! // Multiple conditions with OR
23//! let f = or_filter!(
24//!     filter!(status == "pending"),
25//!     filter!(status == "processing")
26//! );
27//! ```
28
29/// Create a filter expression with minimal allocations.
30///
31/// # Syntax
32///
33/// - `filter!(field == value)` - Equality
34/// - `filter!(field != value)` - Not equals
35/// - `filter!(field > value)` - Greater than
36/// - `filter!(field >= value)` - Greater than or equal
37/// - `filter!(field < value)` - Less than
38/// - `filter!(field <= value)` - Less than or equal
39/// - `filter!(field is null)` - IS NULL
40/// - `filter!(field is not null)` - IS NOT NULL
41/// - `filter!(field contains value)` - LIKE %value%
42/// - `filter!(field starts_with value)` - LIKE value%
43/// - `filter!(field ends_with value)` - LIKE %value
44/// - `filter!(field in [v1, v2, ...])` - IN clause
45/// - `filter!(field not in [v1, v2, ...])` - NOT IN clause
46///
47/// # Examples
48///
49/// ```rust
50/// use prax_query::filter;
51/// use prax_query::filter::Filter;
52///
53/// // Basic comparisons
54/// let f = filter!(id == 42);
55/// let f = filter!(age > 18);
56/// let f = filter!(score >= 100);
57///
58/// // String comparisons
59/// let f = filter!(name == "Alice");
60/// let f = filter!(email contains "@example.com");
61///
62/// // Null checks
63/// let f = filter!(deleted_at is null);
64/// let f = filter!(verified_at is not null);
65///
66/// // IN clauses
67/// let f = filter!(status in ["active", "pending"]);
68/// ```
69#[macro_export]
70macro_rules! filter {
71    // Equality: field == value
72    ($field:ident == $value:expr) => {{
73        $crate::filter::Filter::Equals(
74            ::std::borrow::Cow::Borrowed(stringify!($field)),
75            $value.into(),
76        )
77    }};
78
79    // Not equals: field != value
80    ($field:ident != $value:expr) => {{
81        $crate::filter::Filter::NotEquals(
82            ::std::borrow::Cow::Borrowed(stringify!($field)),
83            $value.into(),
84        )
85    }};
86
87    // Greater than: field > value
88    ($field:ident > $value:expr) => {{
89        $crate::filter::Filter::Gt(
90            ::std::borrow::Cow::Borrowed(stringify!($field)),
91            $value.into(),
92        )
93    }};
94
95    // Greater than or equal: field >= value
96    ($field:ident >= $value:expr) => {{
97        $crate::filter::Filter::Gte(
98            ::std::borrow::Cow::Borrowed(stringify!($field)),
99            $value.into(),
100        )
101    }};
102
103    // Less than: field < value
104    ($field:ident < $value:expr) => {{
105        $crate::filter::Filter::Lt(
106            ::std::borrow::Cow::Borrowed(stringify!($field)),
107            $value.into(),
108        )
109    }};
110
111    // Less than or equal: field <= value
112    ($field:ident <= $value:expr) => {{
113        $crate::filter::Filter::Lte(
114            ::std::borrow::Cow::Borrowed(stringify!($field)),
115            $value.into(),
116        )
117    }};
118
119    // IS NULL: field is null
120    ($field:ident is null) => {{
121        $crate::filter::Filter::IsNull(
122            ::std::borrow::Cow::Borrowed(stringify!($field)),
123        )
124    }};
125
126    // IS NOT NULL: field is not null
127    ($field:ident is not null) => {{
128        $crate::filter::Filter::IsNotNull(
129            ::std::borrow::Cow::Borrowed(stringify!($field)),
130        )
131    }};
132
133    // LIKE %value%: field contains value
134    ($field:ident contains $value:expr) => {{
135        $crate::filter::Filter::Contains(
136            ::std::borrow::Cow::Borrowed(stringify!($field)),
137            $value.into(),
138        )
139    }};
140
141    // LIKE value%: field starts_with value
142    ($field:ident starts_with $value:expr) => {{
143        $crate::filter::Filter::StartsWith(
144            ::std::borrow::Cow::Borrowed(stringify!($field)),
145            $value.into(),
146        )
147    }};
148
149    // LIKE %value: field ends_with value
150    ($field:ident ends_with $value:expr) => {{
151        $crate::filter::Filter::EndsWith(
152            ::std::borrow::Cow::Borrowed(stringify!($field)),
153            $value.into(),
154        )
155    }};
156
157    // IN clause: field in [values]
158    ($field:ident in [$($value:expr),* $(,)?]) => {{
159        $crate::filter::Filter::In(
160            ::std::borrow::Cow::Borrowed(stringify!($field)),
161            ::std::vec![$($value.into()),*],
162        )
163    }};
164
165    // NOT IN clause: field not in [values]
166    ($field:ident not in [$($value:expr),* $(,)?]) => {{
167        $crate::filter::Filter::NotIn(
168            ::std::borrow::Cow::Borrowed(stringify!($field)),
169            ::std::vec![$($value.into()),*],
170        )
171    }};
172}
173
174/// Combine filters with AND.
175///
176/// # Examples
177///
178/// ```rust
179/// use prax_query::{filter, and_filter};
180///
181/// let f = and_filter!(
182///     filter!(active == true),
183///     filter!(score > 100)
184/// );
185///
186/// // Multiple filters
187/// let f = and_filter!(
188///     filter!(status == "active"),
189///     filter!(age >= 18),
190///     filter!(verified == true)
191/// );
192/// ```
193#[macro_export]
194macro_rules! and_filter {
195    // Two filters (optimized)
196    ($a:expr, $b:expr $(,)?) => {{
197        $crate::filter::Filter::And(Box::new([$a, $b]))
198    }};
199
200    // Three filters
201    ($a:expr, $b:expr, $c:expr $(,)?) => {{
202        $crate::filter::Filter::And(Box::new([$a, $b, $c]))
203    }};
204
205    // Four filters
206    ($a:expr, $b:expr, $c:expr, $d:expr $(,)?) => {{
207        $crate::filter::Filter::And(Box::new([$a, $b, $c, $d]))
208    }};
209
210    // Five filters
211    ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr $(,)?) => {{
212        $crate::filter::Filter::And(Box::new([$a, $b, $c, $d, $e]))
213    }};
214
215    // Variable number of filters
216    ($($filter:expr),+ $(,)?) => {{
217        $crate::filter::Filter::And(Box::new([$($filter),+]))
218    }};
219}
220
221/// Combine filters with OR.
222///
223/// # Examples
224///
225/// ```rust
226/// use prax_query::{filter, or_filter};
227///
228/// let f = or_filter!(
229///     filter!(status == "pending"),
230///     filter!(status == "processing")
231/// );
232/// ```
233#[macro_export]
234macro_rules! or_filter {
235    // Two filters (optimized)
236    ($a:expr, $b:expr $(,)?) => {{
237        $crate::filter::Filter::Or(Box::new([$a, $b]))
238    }};
239
240    // Three filters
241    ($a:expr, $b:expr, $c:expr $(,)?) => {{
242        $crate::filter::Filter::Or(Box::new([$a, $b, $c]))
243    }};
244
245    // Variable number of filters
246    ($($filter:expr),+ $(,)?) => {{
247        $crate::filter::Filter::Or(Box::new([$($filter),+]))
248    }};
249}
250
251/// Negate a filter.
252///
253/// # Examples
254///
255/// ```rust
256/// use prax_query::{filter, not_filter};
257///
258/// let f = not_filter!(filter!(deleted == true));
259/// ```
260#[macro_export]
261macro_rules! not_filter {
262    ($filter:expr) => {{ $crate::filter::Filter::Not(Box::new($filter)) }};
263}
264
265#[cfg(test)]
266mod tests {
267    use crate::filter::{Filter, FilterValue};
268
269    #[test]
270    fn test_filter_eq_macro() {
271        let f = filter!(id == 42);
272        match f {
273            Filter::Equals(field, FilterValue::Int(42)) => {
274                assert_eq!(field.as_ref(), "id");
275            }
276            _ => panic!("Expected Equals filter"),
277        }
278    }
279
280    #[test]
281    fn test_filter_ne_macro() {
282        let f = filter!(status != "deleted");
283        assert!(matches!(f, Filter::NotEquals(_, _)));
284    }
285
286    #[test]
287    fn test_filter_gt_macro() {
288        let f = filter!(age > 18);
289        assert!(matches!(f, Filter::Gt(_, FilterValue::Int(18))));
290    }
291
292    #[test]
293    fn test_filter_gte_macro() {
294        let f = filter!(score >= 100);
295        assert!(matches!(f, Filter::Gte(_, FilterValue::Int(100))));
296    }
297
298    #[test]
299    fn test_filter_lt_macro() {
300        let f = filter!(price < 50);
301        assert!(matches!(f, Filter::Lt(_, FilterValue::Int(50))));
302    }
303
304    #[test]
305    fn test_filter_lte_macro() {
306        let f = filter!(quantity <= 10);
307        assert!(matches!(f, Filter::Lte(_, FilterValue::Int(10))));
308    }
309
310    #[test]
311    fn test_filter_is_null_macro() {
312        let f = filter!(deleted_at is null);
313        match f {
314            Filter::IsNull(field) => {
315                assert_eq!(field.as_ref(), "deleted_at");
316            }
317            _ => panic!("Expected IsNull filter"),
318        }
319    }
320
321    #[test]
322    fn test_filter_is_not_null_macro() {
323        let f = filter!(verified_at is not null);
324        match f {
325            Filter::IsNotNull(field) => {
326                assert_eq!(field.as_ref(), "verified_at");
327            }
328            _ => panic!("Expected IsNotNull filter"),
329        }
330    }
331
332    #[test]
333    fn test_filter_contains_macro() {
334        let f = filter!(email contains "@example.com");
335        assert!(matches!(f, Filter::Contains(_, _)));
336    }
337
338    #[test]
339    fn test_filter_starts_with_macro() {
340        let f = filter!(name starts_with "John");
341        assert!(matches!(f, Filter::StartsWith(_, _)));
342    }
343
344    #[test]
345    fn test_filter_ends_with_macro() {
346        let f = filter!(email ends_with ".com");
347        assert!(matches!(f, Filter::EndsWith(_, _)));
348    }
349
350    #[test]
351    fn test_filter_in_macro() {
352        let f = filter!(status in ["active", "pending", "processing"]);
353        match f {
354            Filter::In(field, values) => {
355                assert_eq!(field.as_ref(), "status");
356                assert_eq!(values.len(), 3);
357            }
358            _ => panic!("Expected In filter"),
359        }
360    }
361
362    #[test]
363    fn test_filter_not_in_macro() {
364        let f = filter!(role not in ["admin", "superuser"]);
365        match f {
366            Filter::NotIn(field, values) => {
367                assert_eq!(field.as_ref(), "role");
368                assert_eq!(values.len(), 2);
369            }
370            _ => panic!("Expected NotIn filter"),
371        }
372    }
373
374    #[test]
375    fn test_and_filter_macro() {
376        let f = and_filter!(filter!(active == true), filter!(score > 100));
377        match f {
378            Filter::And(filters) => {
379                assert_eq!(filters.len(), 2);
380            }
381            _ => panic!("Expected And filter"),
382        }
383    }
384
385    #[test]
386    fn test_or_filter_macro() {
387        let f = or_filter!(
388            filter!(status == "pending"),
389            filter!(status == "processing")
390        );
391        match f {
392            Filter::Or(filters) => {
393                assert_eq!(filters.len(), 2);
394            }
395            _ => panic!("Expected Or filter"),
396        }
397    }
398
399    #[test]
400    fn test_not_filter_macro() {
401        let f = not_filter!(filter!(deleted == true));
402        assert!(matches!(f, Filter::Not(_)));
403    }
404
405    #[test]
406    fn test_complex_filter_macro() {
407        // Complex nested filter
408        let f = and_filter!(
409            filter!(active == true),
410            or_filter!(filter!(role == "admin"), filter!(role == "moderator")),
411            filter!(age >= 18)
412        );
413        assert!(matches!(f, Filter::And(_)));
414    }
415}