lambda_appsync/
subscription_filters.rs

1//! GraphQL subscription filter implementation for AWS AppSync
2//!
3//! This module provides types and abstractions for building type-safe GraphQL
4//! subscription filters according to the AWS AppSync specification. The filters
5//! are used to control which events are delivered to subscribed clients based
6//! on the event payloads.
7//!
8//! The module enforces AWS AppSync's filter constraints at compile time, including:
9//! - Maximum depth of 5 levels for nested field paths
10//! - Maximum 256 character length for field paths
11//! - `in` and `notIn` operators accept up to 5 values in an array
12//! - `containsAny` operator accepts up to 20 values in an array
13//!
14//! # Examples
15//!
16//! Simple field equality filter:
17//! ```
18//! # use lambda_appsync::{subscription_filters::FieldPath, AppsyncError};
19//! # fn example() -> Result<(), AppsyncError> {
20//! let filter = FieldPath::new("user.name")?.eq("example");
21//! # Ok(())
22//! # }
23//! ```
24//!
25//! Complex filter group with AND/OR logic:
26//! ```
27//! # use lambda_appsync::{subscription_filters::{FieldPath, Filter, FilterGroup}, AppsyncError};
28//! # fn example() -> Result<FilterGroup, AppsyncError> {
29//! // The FilterGroup combines Filter elements with OR logic
30//! // This means the filter will match if ANY of the Filter conditions are true
31//! let group = FilterGroup::from([
32//!     // First filter - combines conditions with AND logic:
33//!     // - user.role must equal "admin" AND
34//!     // - user.age must be greater than 21
35//!     Filter::from([
36//!         FieldPath::new("user.role")?.eq("admin"),
37//!         FieldPath::new("user.age")?.gt(21)
38//!     ]),
39//!     // Second filter - also uses AND logic between its conditions:
40//!     // - user.role must equal "moderator" AND
41//!     // - user.permissions must contain either "moderate" or "review"
42//!     Filter::from([
43//!         FieldPath::new("user.role")?.eq("moderator"),
44//!         FieldPath::new("user.permissions")?.contains_any(["moderate", "review"])
45//!     ])
46//!     // Final logic:
47//!     // (role="admin" AND age>21) OR (role="moderator" AND permissions∩["moderate","review"]≠∅)
48//! ]);
49//! # Ok(group)
50//! # }
51//! ```
52//!
53//! Array operators with size limits:
54//! ```
55//! # use lambda_appsync::{subscription_filters::FieldPath, AppsyncError};
56//! # fn example() -> Result<(), AppsyncError> {
57//! // IN operator (max 5 values)
58//! let roles = FieldPath::new("user.role")?.in_values(["admin", "mod", "user"]);
59//!
60//! // ContainsAny operator (max 20 values)
61//! let perms = FieldPath::new("user.permissions")?
62//!     .contains_any(["read", "write", "delete", "admin"]);
63//! # Ok(())
64//! # }
65//! ```
66
67use serde::Serialize;
68
69use crate::{
70    AWSDate, AWSDateTime, AWSEmail, AWSPhone, AWSTime, AWSTimestamp, AWSUrl, AppsyncError, ID,
71};
72
73/// Private marker trait for types that can be used in filter values
74pub trait IFSValueMarker: private::Sealed + Serialize {
75    /// Convert the value to a serde_json::Value
76    fn to_value(&self) -> serde_json::Value {
77        serde_json::to_value(self).expect("cannot fail for IFSValueMarker types")
78    }
79}
80
81/// Private marker trait for types that can be used in equality operations
82pub trait IFSBValueMarker: private::Sealed + Serialize {
83    /// Convert the value to a serde_json::Value
84    fn to_value(&self) -> serde_json::Value {
85        serde_json::to_value(self).expect("cannot fail for IFSBValueMarker types")
86    }
87}
88
89mod private {
90    pub trait Sealed {}
91}
92
93macro_rules! impl_markers {
94    (nested $tr:ty, ($($t:ty),+)) => {
95        $(impl $tr for $t {})+
96    };
97    ($($tr:ty),+| $t:tt) => {
98        $(impl_markers!(nested $tr, $t);)+
99    }
100}
101impl_markers!(
102    IFSBValueMarker,
103    private::Sealed
104        | (
105            u8,
106            i8,
107            u16,
108            i16,
109            u32,
110            i32,
111            u64,
112            i64,
113            u128,
114            i128,
115            f32,
116            f64,
117            bool,
118            String,
119            &str,
120            ID,
121            AWSEmail,
122            AWSUrl,
123            AWSDate,
124            AWSTime,
125            AWSPhone,
126            AWSDateTime,
127            AWSTimestamp
128        )
129);
130impl_markers!(
131    IFSValueMarker
132        | (
133            u8,
134            i8,
135            u16,
136            i16,
137            u32,
138            i32,
139            u64,
140            i64,
141            u128,
142            i128,
143            f32,
144            f64,
145            String,
146            &str,
147            ID,
148            AWSEmail,
149            AWSUrl,
150            AWSDate,
151            AWSTime,
152            AWSPhone,
153            AWSDateTime,
154            AWSTimestamp
155        )
156);
157
158/// Fixed-size vector for operators with size limits
159#[derive(Debug, Clone, PartialEq)]
160pub struct FixedVec<T, const N: usize>([Option<T>; N]);
161
162impl<T: Serialize, const N: usize> Serialize for FixedVec<T, N> {
163    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
164    where
165        S: serde::Serializer,
166    {
167        serializer.collect_seq(self.0.iter().flatten())
168    }
169}
170impl<T: IFSValueMarker, const N: usize> FixedVec<T, N> {
171    fn to_value(&self) -> serde_json::Value {
172        serde_json::to_value(self).expect("cannot fail for IFSValueMarker types")
173    }
174}
175
176/// A vector limited to 5 elements for In/NotIn operators
177type InVec<T> = FixedVec<T, 5>;
178
179/// A vector limited to 20 elements for ContainsAny operator
180type ContainsAnyVec<T> = FixedVec<T, 20>;
181
182macro_rules! impl_from_array {
183    (none 5) => {
184        [None, None, None, None, None]
185    };
186    (none 10) => {
187        [None, None, None, None, None, None, None, None, None, None]
188    };
189    (none 20) => {
190        [
191            None, None, None, None, None, None, None, None, None, None, None, None, None, None,
192            None, None, None, None, None, None,
193        ]
194    };
195    ($m:tt; $n:literal; $(($idx:tt, $v:ident)),*) => {
196        impl<T> From<[T; $n]> for FixedVec<T, $m> {
197            fn from([$($v),*]: [T; $n]) -> Self {
198                let mut slice = impl_from_array!(none $m);
199                $((slice[$idx]).replace($v);)*
200                Self(slice)
201            }
202        }
203    };
204}
205impl_from_array!(5; 1; (0, v1));
206impl_from_array!(5; 2; (0, v1), (1, v2));
207impl_from_array!(5; 3; (0, v1), (1, v2), (2, v3));
208impl_from_array!(5; 4; (0, v1), (1, v2), (2, v3), (3, v4));
209impl_from_array!(5; 5; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5));
210impl_from_array!(10; 1; (0, v1));
211impl_from_array!(10; 2; (0, v1), (1, v2));
212impl_from_array!(10; 3; (0, v1), (1, v2), (2, v3));
213impl_from_array!(10; 4; (0, v1), (1, v2), (2, v3), (3, v4));
214impl_from_array!(10; 5; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5));
215impl_from_array!(10; 6; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6));
216impl_from_array!(10; 7; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7));
217impl_from_array!(10; 8; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8));
218impl_from_array!(10; 9; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9));
219impl_from_array!(10; 10; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10));
220impl_from_array!(20; 1; (0, v1));
221impl_from_array!(20; 2; (0, v1), (1, v2));
222impl_from_array!(20; 3; (0, v1), (1, v2), (2, v3));
223impl_from_array!(20; 4; (0, v1), (1, v2), (2, v3), (3, v4));
224impl_from_array!(20; 5; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5));
225impl_from_array!(20; 6; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6));
226impl_from_array!(20; 7; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7));
227impl_from_array!(20; 8; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8));
228impl_from_array!(20; 9; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9));
229impl_from_array!(20; 10; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10));
230impl_from_array!(20; 11; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11));
231impl_from_array!(20; 12; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12));
232impl_from_array!(20; 13; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13));
233impl_from_array!(20; 14; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14));
234impl_from_array!(20; 15; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15));
235impl_from_array!(20; 16; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15), (15, v16));
236impl_from_array!(20; 17; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15), (15, v16), (16, v17));
237impl_from_array!(20; 18; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15), (15, v16), (16, v17), (17, v18));
238impl_from_array!(20; 19; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15), (15, v16), (16, v17), (17, v18), (18, v19));
239impl_from_array!(20; 20; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15), (15, v16), (16, v17), (17, v18), (18, v19), (19, v20));
240
241/// Field path supporting up to 5 levels of nesting
242#[derive(Debug, Clone, PartialEq, Serialize)]
243#[serde(transparent)]
244pub struct FieldPath(String);
245
246impl std::fmt::Display for FieldPath {
247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        write!(f, "{}", self.0)
249    }
250}
251
252impl FieldPath {
253    /// Creates a new field path from a string-like value
254    ///
255    /// # Arguments
256    /// * `path` - Field path as a string
257    ///
258    /// # Errors
259    /// Returns ValidationError if path exceeds 256 characters
260    ///
261    /// # Examples
262    /// ```
263    /// # use lambda_appsync::{AppsyncError, subscription_filters::FieldPath};
264    /// # fn example() -> Result<(), AppsyncError> {
265    /// let path = FieldPath::new("user.profile.name")?;
266    /// # Ok(())
267    /// # }
268    /// ```
269    pub fn new(path: impl Into<String>) -> Result<Self, AppsyncError> {
270        let path = path.into();
271        if path.len() > 256 {
272            return Err(AppsyncError::new(
273                "ValidationError",
274                "Field path exceeds 256 characters",
275            ));
276        }
277        // Could add more validation here
278        Ok(Self(path))
279    }
280
281    /// Creates a new field path from a string-like value without validation
282    ///
283    /// # Safety
284    /// This function skips validation of the field path. The caller must ensure:
285    /// - Path length does not exceed 256 characters
286    /// - Path contains valid field references only
287    /// - Path nesting depth does not exceed 5 levels
288    ///
289    /// # Examples
290    /// ```
291    /// # use lambda_appsync::subscription_filters::FieldPath;
292    /// let path = unsafe { FieldPath::new_unchecked("user.name") };
293    /// ```
294    pub unsafe fn new_unchecked(path: impl Into<String>) -> Self {
295        Self(path.into())
296    }
297
298    // IFSB operators
299    /// Creates an equality filter
300    pub fn eq<IFSB: IFSBValueMarker>(self, ifsb: IFSB) -> FieldFilter {
301        FieldFilter::new(self, ifsb.to_value(), FilterOp::Eq)
302    }
303
304    /// Creates an equality filter from any serializable value
305    ///
306    /// # Safety
307    /// The caller must ensure the value serializes to either:
308    /// - A number
309    /// - A string
310    /// - A boolean
311    pub unsafe fn eq_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
312        FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Eq)
313    }
314
315    /// Creates a not equal filter
316    pub fn ne<IFSB: IFSBValueMarker>(self, ifsb: IFSB) -> FieldFilter {
317        FieldFilter::new(self, ifsb.to_value(), FilterOp::Ne)
318    }
319
320    /// Creates a not equal filter from any serializable value
321    ///
322    /// # Safety
323    /// The caller must ensure the value serializes to either:
324    /// - A number
325    /// - A string
326    /// - A boolean
327    pub unsafe fn ne_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
328        FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Ne)
329    }
330
331    // IFS operators
332    /// Creates a less than or equal filter
333    pub fn le<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
334        FieldFilter::new(self, ifs.to_value(), FilterOp::Le)
335    }
336
337    /// Creates a less than or equal filter from any serializable value
338    ///
339    /// # Safety
340    /// The caller must ensure the value serializes to either:
341    /// - A number
342    /// - A string
343    pub unsafe fn le_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
344        FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Le)
345    }
346
347    /// Creates a less than filter
348    pub fn lt<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
349        FieldFilter::new(self, ifs.to_value(), FilterOp::Lt)
350    }
351
352    /// Creates a less than filter from any serializable value
353    ///
354    /// # Safety
355    /// The caller must ensure the value serializes to either:
356    /// - A number
357    /// - A string
358    pub unsafe fn lt_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
359        FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Lt)
360    }
361
362    /// Creates a greater than or equal filter
363    pub fn ge<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
364        FieldFilter::new(self, ifs.to_value(), FilterOp::Ge)
365    }
366
367    /// Creates a greater than or equal filter from any serializable value
368    ///
369    /// # Safety
370    /// The caller must ensure the value serializes to either:
371    /// - A number
372    /// - A string
373    pub unsafe fn ge_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
374        FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Ge)
375    }
376
377    /// Creates a greater than filter
378    pub fn gt<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
379        FieldFilter::new(self, ifs.to_value(), FilterOp::Gt)
380    }
381
382    /// Creates a greater than filter from any serializable value
383    ///
384    /// # Safety
385    /// The caller must ensure the value serializes to either:
386    /// - A number
387    /// - A string
388    pub unsafe fn gt_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
389        FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Gt)
390    }
391
392    /// Creates a contains filter for strings or arrays
393    pub fn contains<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
394        FieldFilter::new(self, ifs.to_value(), FilterOp::Contains)
395    }
396
397    /// Creates a contains filter from any serializable value
398    ///
399    /// # Safety
400    /// The caller must ensure the value serializes to either:
401    /// - A number
402    /// - A string
403    pub unsafe fn contains_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
404        FieldFilter::new(
405            self,
406            serde_json::to_value(value).unwrap(),
407            FilterOp::Contains,
408        )
409    }
410
411    /// Creates a not contains filter for strings or arrays
412    pub fn not_contains<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
413        FieldFilter::new(self, ifs.to_value(), FilterOp::NotContains)
414    }
415
416    /// Creates a not contains filter from any serializable value
417    ///
418    /// # Safety
419    /// The caller must ensure the value serializes to either:
420    /// - A number
421    /// - A string
422    pub unsafe fn not_contains_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
423        FieldFilter::new(
424            self,
425            serde_json::to_value(value).unwrap(),
426            FilterOp::NotContains,
427        )
428    }
429
430    // String only
431    /// Creates a begins with filter for string fields
432    pub fn begins_with(self, value: impl Into<String>) -> FieldFilter {
433        FieldFilter::new(
434            self,
435            serde_json::Value::String(value.into()),
436            FilterOp::BeginsWith,
437        )
438    }
439
440    // Array operators
441    /// Creates an IN filter accepting up to 5 values
442    ///
443    /// # Examples
444    /// ```
445    /// # use lambda_appsync::{AppsyncError, subscription_filters::FieldPath};
446    /// # fn example() -> Result<(), AppsyncError> {
447    /// let path = FieldPath::new("user.id")?;
448    /// let filter = path.in_values(["id1", "id2", "id3"]);
449    /// # Ok(())
450    /// # }
451    /// ```
452    pub fn in_values<IFS: IFSValueMarker>(self, values: impl Into<InVec<IFS>>) -> FieldFilter {
453        let in_vec = values.into();
454        FieldFilter::new(self, in_vec.to_value(), FilterOp::In)
455    }
456
457    /// Creates an IN filter from any array of up to 5 serializable values
458    ///
459    /// # Safety
460    /// The caller must ensure each value in the array serializes to either:
461    /// - A number
462    /// - A string
463    pub unsafe fn in_values_unchecked<T: Serialize>(
464        self,
465        values: impl Into<InVec<T>>,
466    ) -> FieldFilter {
467        let in_vec = values.into();
468        FieldFilter::new(self, serde_json::to_value(in_vec).unwrap(), FilterOp::In)
469    }
470
471    /// Creates a NOT IN filter accepting up to 5 values
472    ///
473    /// # Examples
474    /// ```
475    /// # use lambda_appsync::{AppsyncError, subscription_filters::FieldPath};
476    /// # fn example() -> Result<(), AppsyncError> {
477    /// let path = FieldPath::new("user.role")?;
478    /// let filter = path.not_in(["admin", "moderator"]);
479    /// # Ok(())
480    /// # }
481    /// ```
482    pub fn not_in<IFS: IFSValueMarker>(self, values: impl Into<InVec<IFS>>) -> FieldFilter {
483        let in_vec = values.into();
484        FieldFilter::new(self, in_vec.to_value(), FilterOp::NotIn)
485    }
486
487    /// Creates a NOT IN filter from any array of up to 5 serializable values
488    ///
489    /// # Safety
490    /// The caller must ensure values in the array serializes to either:
491    /// - Numbers
492    /// - Strings
493    pub unsafe fn not_in_unchecked<T: Serialize>(self, values: impl Into<InVec<T>>) -> FieldFilter {
494        let in_vec = values.into();
495        FieldFilter::new(self, serde_json::to_value(in_vec).unwrap(), FilterOp::NotIn)
496    }
497
498    /// Creates a BETWEEN filter that matches values in a range
499    pub fn between<IFS: IFSValueMarker>(self, start: IFS, end: IFS) -> FieldFilter {
500        FieldFilter::new(
501            self,
502            FixedVec([Some(start), Some(end)]).to_value(),
503            FilterOp::Between,
504        )
505    }
506
507    /// Creates a BETWEEN filter from any two serializable values
508    ///
509    /// # Safety
510    /// The caller must ensure both values serialize to either:
511    /// - Numbers
512    /// - Strings
513    pub unsafe fn between_unchecked<T: Serialize>(self, start: T, end: T) -> FieldFilter {
514        FieldFilter::new(
515            self,
516            serde_json::to_value(FixedVec([Some(start), Some(end)])).unwrap(),
517            FilterOp::Between,
518        )
519    }
520
521    /// Creates a contains any filter accepting up to 20 values
522    ///
523    /// # Examples
524    /// ```
525    /// # use lambda_appsync::{AppsyncError, subscription_filters::FieldPath};
526    /// # fn example() -> Result<(), AppsyncError> {
527    /// let path = FieldPath::new("user.permissions")?;
528    /// let filter = path.contains_any(["read", "write", "delete"]);
529    /// # Ok(())
530    /// # }
531    /// ```
532    pub fn contains_any<IFS: IFSValueMarker>(
533        self,
534        values: impl Into<ContainsAnyVec<IFS>>,
535    ) -> FieldFilter {
536        let contains_vec = values.into();
537        FieldFilter::new(self, contains_vec.to_value(), FilterOp::ContainsAny)
538    }
539
540    /// Creates a contains any filter from any array of up to 20 serializable values
541    ///
542    /// # Safety
543    /// The caller must ensure values in the array serializes to either:
544    /// - Numbers
545    /// - Strings
546    pub unsafe fn contains_any_unchecked<T: Serialize>(
547        self,
548        values: impl Into<ContainsAnyVec<T>>,
549    ) -> FieldFilter {
550        let contains_vec = values.into();
551        FieldFilter::new(
552            self,
553            serde_json::to_value(contains_vec).unwrap(),
554            FilterOp::ContainsAny,
555        )
556    }
557}
558
559#[derive(Debug, Clone, Serialize)]
560#[serde(rename_all = "camelCase")]
561enum FilterOp {
562    Eq,
563    Ne,
564    Le,
565    Lt,
566    Ge,
567    Gt,
568    Contains,
569    NotContains,
570    BeginsWith,
571    In,
572    NotIn,
573    Between,
574    ContainsAny,
575}
576
577/// A single field filter that combines a field path with an operator and value
578/// in the AppSync subscription filter format.
579///
580/// Should be created from a [FieldPath] using the operator methods:
581///
582/// # Example
583/// ```
584/// # use lambda_appsync::{AppsyncError, subscription_filters::{FieldPath, FieldFilter}};
585/// # fn example() -> Result<FieldFilter, AppsyncError> {
586/// let path = FieldPath::new("user.name")?;
587/// let filter = path.eq("example");
588/// # Ok(filter)
589/// # }
590/// ```
591#[derive(Debug, Clone, Serialize)]
592pub struct FieldFilter {
593    #[serde(rename = "fieldName")]
594    path: FieldPath,
595    operator: FilterOp,
596    value: serde_json::Value,
597}
598impl FieldFilter {
599    fn new(path: FieldPath, value: serde_json::Value, operator: FilterOp) -> Self {
600        Self {
601            path,
602            value,
603            operator,
604        }
605    }
606}
607/// A single filter limited to 5 field filters
608///
609/// Can be created from an arrays of up to 5 [FieldFilter] elements.
610///
611/// # Example
612/// ```
613/// # use lambda_appsync::{subscription_filters::{FieldPath, Filter}, AppsyncError};
614/// # fn example() -> Result<Filter, AppsyncError> {
615/// let filter = Filter::from([
616///     FieldPath::new("user.name")?.eq("test"),
617///     FieldPath::new("user.age")?.gt(21),
618///     FieldPath::new("user.role")?.in_values(["admin", "moderator"])
619/// ]);
620/// # Ok(filter)
621/// # }
622/// ```
623#[derive(Debug, Clone, Serialize)]
624pub struct Filter {
625    filters: FixedVec<FieldFilter, 5>,
626}
627
628impl<T> From<T> for Filter
629where
630    T: Into<FixedVec<FieldFilter, 5>>,
631{
632    fn from(filters: T) -> Self {
633        Self {
634            filters: filters.into(),
635        }
636    }
637}
638
639impl From<FieldFilter> for Filter {
640    fn from(value: FieldFilter) -> Self {
641        Filter::from([value])
642    }
643}
644
645/// A filter group limited to 10 filters combined with OR logic
646///
647/// Can be created from an arrays of up to 10 [Filter] elements.
648///
649/// # Example
650/// ```
651/// # use lambda_appsync::{subscription_filters::{FieldPath, Filter, FilterGroup}, AppsyncError};
652/// # fn example() -> Result<FilterGroup, AppsyncError> {
653/// let group = FilterGroup::from([
654///     // First filter - user is admin AND age over 21
655///     Filter::from([
656///         FieldPath::new("user.role")?.eq("admin"),
657///         FieldPath::new("user.age")?.gt(21)
658///     ]),
659///     // Second filter - user is moderator AND has required permissions
660///     Filter::from([
661///         FieldPath::new("user.role")?.eq("moderator"),
662///         FieldPath::new("user.permissions")?.contains_any(["moderate", "review"])
663///     ])
664/// ]);
665/// # Ok(group)
666/// # }
667/// ```
668#[derive(Debug, Clone, Serialize)]
669pub struct FilterGroup {
670    #[serde(rename = "filterGroup")]
671    filters: FixedVec<Filter, 10>,
672}
673
674impl<T> From<T> for FilterGroup
675where
676    T: Into<FixedVec<Filter, 10>>,
677{
678    fn from(filters: T) -> Self {
679        Self {
680            filters: filters.into(),
681        }
682    }
683}
684
685impl From<FieldFilter> for FilterGroup {
686    fn from(value: FieldFilter) -> Self {
687        FilterGroup::from(Filter::from([value]))
688    }
689}
690impl From<Filter> for FilterGroup {
691    fn from(value: Filter) -> Self {
692        FilterGroup::from([value])
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699    use serde_json::json;
700
701    fn filter_value(f: &FieldFilter) -> serde_json::Value {
702        serde_json::to_value(f).unwrap()
703    }
704
705    #[test]
706    fn test_create_paths() {
707        let path = FieldPath::new("user.name").unwrap();
708        assert_eq!(path.to_string(), "user.name");
709
710        let path = FieldPath::new("nested.one.two.three.four.five");
711        assert!(path.is_ok());
712
713        let long_path = "a".repeat(257);
714        assert!(FieldPath::new(long_path).is_err());
715    }
716
717    #[test]
718    fn test_eq_operator() {
719        // Test string equality
720        let filter = FieldPath::new("service").unwrap().eq("AWS AppSync");
721        assert_eq!(
722            filter_value(&filter),
723            json!({
724                "fieldName": "service",
725                "operator": "eq",
726                "value": "AWS AppSync"
727            })
728        );
729
730        // Test numeric equality
731        let filter = FieldPath::new("severity").unwrap().eq(5);
732        assert_eq!(
733            filter_value(&filter),
734            json!({
735                "fieldName": "severity",
736                "operator": "eq",
737                "value": 5
738            })
739        );
740
741        // Test boolean equality
742        let filter = FieldPath::new("enabled").unwrap().eq(true);
743        assert_eq!(
744            filter_value(&filter),
745            json!({
746                "fieldName": "enabled",
747                "operator": "eq",
748                "value": true
749            })
750        );
751    }
752
753    #[test]
754    fn test_ne_operator() {
755        // Test string inequality
756        let filter = FieldPath::new("service").unwrap().ne("AWS AppSync");
757        assert_eq!(
758            filter_value(&filter),
759            json!({
760                "fieldName": "service",
761                "operator": "ne",
762                "value": "AWS AppSync"
763            })
764        );
765
766        // Test numeric inequality
767        let filter = FieldPath::new("severity").unwrap().ne(5);
768        assert_eq!(
769            filter_value(&filter),
770            json!({
771                "fieldName": "severity",
772                "operator": "ne",
773                "value": 5
774            })
775        );
776
777        // Test boolean inequality
778        let filter = FieldPath::new("enabled").unwrap().ne(true);
779        assert_eq!(
780            filter_value(&filter),
781            json!({
782                "fieldName": "enabled",
783                "operator": "ne",
784                "value": true
785            })
786        );
787    }
788
789    #[test]
790    fn test_comparison_operators() {
791        let path = FieldPath::new("size").unwrap();
792
793        // Test le
794        let filter = path.clone().le(5);
795        assert_eq!(
796            filter_value(&filter),
797            json!({
798                "fieldName": "size",
799                "operator": "le",
800                "value": 5
801            })
802        );
803
804        // Test lt
805        let filter = path.clone().lt(5);
806        assert_eq!(
807            filter_value(&filter),
808            json!({
809                "fieldName": "size",
810                "operator": "lt",
811                "value": 5
812            })
813        );
814
815        // Test ge
816        let filter = path.clone().ge(5);
817        assert_eq!(
818            filter_value(&filter),
819            json!({
820                "fieldName": "size",
821                "operator": "ge",
822                "value": 5
823            })
824        );
825
826        // Test gt
827        let filter = path.gt(5);
828        assert_eq!(
829            filter_value(&filter),
830            json!({
831                "fieldName": "size",
832                "operator": "gt",
833                "value": 5
834            })
835        );
836    }
837
838    #[test]
839    fn test_contains_operators() {
840        let path = FieldPath::new("seats").unwrap();
841
842        // Test contains with number
843        let filter = path.clone().contains(10);
844        assert_eq!(
845            filter_value(&filter),
846            json!({
847                "fieldName": "seats",
848                "operator": "contains",
849                "value": 10
850            })
851        );
852
853        // Test contains with string
854        let event_path = FieldPath::new("event").unwrap();
855        let filter = event_path.clone().contains("launch");
856        assert_eq!(
857            filter_value(&filter),
858            json!({
859                "fieldName": "event",
860                "operator": "contains",
861                "value": "launch"
862            })
863        );
864
865        // Test notContains
866        let filter = path.not_contains(10);
867        assert_eq!(
868            filter_value(&filter),
869            json!({
870                "fieldName": "seats",
871                "operator": "notContains",
872                "value": 10
873            })
874        );
875    }
876
877    #[test]
878    fn test_begins_with() {
879        let filter = FieldPath::new("service").unwrap().begins_with("AWS");
880        assert_eq!(
881            filter_value(&filter),
882            json!({
883                "fieldName": "service",
884                "operator": "beginsWith",
885                "value": "AWS"
886            })
887        );
888    }
889
890    #[test]
891    fn test_in_operators() {
892        let path = FieldPath::new("severity").unwrap();
893
894        // Test in with up to 5 values
895        let filter = path.clone().in_values([1, 2, 3]);
896        assert_eq!(
897            filter_value(&filter),
898            json!({
899                "fieldName": "severity",
900                "operator": "in",
901                "value": [1, 2, 3]
902            })
903        );
904
905        // Test notIn with up to 5 values
906        let filter = path.not_in([1, 2, 3]);
907        assert_eq!(
908            filter_value(&filter),
909            json!({
910                "fieldName": "severity",
911                "operator": "notIn",
912                "value": [1, 2, 3]
913            })
914        );
915    }
916
917    #[test]
918    fn test_between() {
919        let filter = FieldPath::new("severity").unwrap().between(1, 5);
920        assert_eq!(
921            filter_value(&filter),
922            json!({
923                "fieldName": "severity",
924                "operator": "between",
925                "value": [1, 5]
926            })
927        );
928    }
929
930    #[test]
931    fn test_contains_any() {
932        let filter = FieldPath::new("seats").unwrap().contains_any([10, 15, 20]);
933        assert_eq!(
934            filter_value(&filter),
935            json!({
936                "fieldName": "seats",
937                "operator": "containsAny",
938                "value": [10, 15, 20]
939            })
940        );
941    }
942
943    #[test]
944    fn test_filter_group() {
945        let group = FilterGroup::from([Filter::from([
946            FieldPath::new("userId").unwrap().eq(1),
947            FieldPath::new("group")
948                .unwrap()
949                .in_values(["Admin", "Developer"]),
950        ])]);
951
952        assert_eq!(
953            serde_json::to_value(group).unwrap(),
954            json!({
955                "filterGroup": [
956                    {
957                        "filters": [
958                            {
959                                "fieldName": "userId",
960                                "operator": "eq",
961                                "value": 1
962                            },
963                            {
964                                "fieldName": "group",
965                                "operator": "in",
966                                "value": ["Admin", "Developer"]
967                            }
968                        ]
969                    }
970                ]
971            })
972        );
973    }
974
975    #[test]
976    fn test_complex_filter() {
977        let group = FilterGroup::from([
978            Filter::from([
979                FieldPath::new("severity").unwrap().le(3),
980                FieldPath::new("type").unwrap().eq("error"),
981            ]),
982            Filter::from([
983                FieldPath::new("service").unwrap().begins_with("AWS"),
984                FieldPath::new("region")
985                    .unwrap()
986                    .in_values(["us-east-1", "eu-west-1"]),
987            ]),
988        ]);
989
990        assert_eq!(
991            serde_json::to_value(group).unwrap(),
992            json!({
993                "filterGroup": [
994                    {
995                        "filters": [
996                            {
997                                "fieldName": "severity",
998                                "operator": "le",
999                                "value": 3
1000                            },
1001                            {
1002                                "fieldName": "type",
1003                                "operator": "eq",
1004                                "value": "error"
1005                            }
1006                        ]
1007                    },
1008                    {
1009                        "filters": [
1010                            {
1011                                "fieldName": "service",
1012                                "operator": "beginsWith",
1013                                "value": "AWS"
1014                            },
1015                            {
1016                                "fieldName": "region",
1017                                "operator": "in",
1018                                "value": ["us-east-1", "eu-west-1"]
1019                            }
1020                        ]
1021                    }
1022                ]
1023            })
1024        );
1025    }
1026}