Skip to main content

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