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) => {{
263        $crate::filter::Filter::Not(Box::new($filter))
264    }};
265}
266
267#[cfg(test)]
268mod tests {
269    use crate::filter::{Filter, FilterValue};
270
271    #[test]
272    fn test_filter_eq_macro() {
273        let f = filter!(id == 42);
274        match f {
275            Filter::Equals(field, FilterValue::Int(42)) => {
276                assert_eq!(field.as_ref(), "id");
277            }
278            _ => panic!("Expected Equals filter"),
279        }
280    }
281
282    #[test]
283    fn test_filter_ne_macro() {
284        let f = filter!(status != "deleted");
285        assert!(matches!(f, Filter::NotEquals(_, _)));
286    }
287
288    #[test]
289    fn test_filter_gt_macro() {
290        let f = filter!(age > 18);
291        assert!(matches!(f, Filter::Gt(_, FilterValue::Int(18))));
292    }
293
294    #[test]
295    fn test_filter_gte_macro() {
296        let f = filter!(score >= 100);
297        assert!(matches!(f, Filter::Gte(_, FilterValue::Int(100))));
298    }
299
300    #[test]
301    fn test_filter_lt_macro() {
302        let f = filter!(price < 50);
303        assert!(matches!(f, Filter::Lt(_, FilterValue::Int(50))));
304    }
305
306    #[test]
307    fn test_filter_lte_macro() {
308        let f = filter!(quantity <= 10);
309        assert!(matches!(f, Filter::Lte(_, FilterValue::Int(10))));
310    }
311
312    #[test]
313    fn test_filter_is_null_macro() {
314        let f = filter!(deleted_at is null);
315        match f {
316            Filter::IsNull(field) => {
317                assert_eq!(field.as_ref(), "deleted_at");
318            }
319            _ => panic!("Expected IsNull filter"),
320        }
321    }
322
323    #[test]
324    fn test_filter_is_not_null_macro() {
325        let f = filter!(verified_at is not null);
326        match f {
327            Filter::IsNotNull(field) => {
328                assert_eq!(field.as_ref(), "verified_at");
329            }
330            _ => panic!("Expected IsNotNull filter"),
331        }
332    }
333
334    #[test]
335    fn test_filter_contains_macro() {
336        let f = filter!(email contains "@example.com");
337        assert!(matches!(f, Filter::Contains(_, _)));
338    }
339
340    #[test]
341    fn test_filter_starts_with_macro() {
342        let f = filter!(name starts_with "John");
343        assert!(matches!(f, Filter::StartsWith(_, _)));
344    }
345
346    #[test]
347    fn test_filter_ends_with_macro() {
348        let f = filter!(email ends_with ".com");
349        assert!(matches!(f, Filter::EndsWith(_, _)));
350    }
351
352    #[test]
353    fn test_filter_in_macro() {
354        let f = filter!(status in ["active", "pending", "processing"]);
355        match f {
356            Filter::In(field, values) => {
357                assert_eq!(field.as_ref(), "status");
358                assert_eq!(values.len(), 3);
359            }
360            _ => panic!("Expected In filter"),
361        }
362    }
363
364    #[test]
365    fn test_filter_not_in_macro() {
366        let f = filter!(role not in ["admin", "superuser"]);
367        match f {
368            Filter::NotIn(field, values) => {
369                assert_eq!(field.as_ref(), "role");
370                assert_eq!(values.len(), 2);
371            }
372            _ => panic!("Expected NotIn filter"),
373        }
374    }
375
376    #[test]
377    fn test_and_filter_macro() {
378        let f = and_filter!(
379            filter!(active == true),
380            filter!(score > 100)
381        );
382        match f {
383            Filter::And(filters) => {
384                assert_eq!(filters.len(), 2);
385            }
386            _ => panic!("Expected And filter"),
387        }
388    }
389
390    #[test]
391    fn test_or_filter_macro() {
392        let f = or_filter!(
393            filter!(status == "pending"),
394            filter!(status == "processing")
395        );
396        match f {
397            Filter::Or(filters) => {
398                assert_eq!(filters.len(), 2);
399            }
400            _ => panic!("Expected Or filter"),
401        }
402    }
403
404    #[test]
405    fn test_not_filter_macro() {
406        let f = not_filter!(filter!(deleted == true));
407        assert!(matches!(f, Filter::Not(_)));
408    }
409
410    #[test]
411    fn test_complex_filter_macro() {
412        // Complex nested filter
413        let f = and_filter!(
414            filter!(active == true),
415            or_filter!(
416                filter!(role == "admin"),
417                filter!(role == "moderator")
418            ),
419            filter!(age >= 18)
420        );
421        assert!(matches!(f, Filter::And(_)));
422    }
423}
424