Skip to main content

dynamodb_facade/expressions/
conditions.rs

1use core::fmt;
2use std::borrow::Cow;
3
4use super::{
5    super::IntoAttributeValue, ApplyCondition, ApplyExpressionAttributes, ApplyFilter,
6    ApplyKeyCondition, AttrNames, AttrValues, AttributeValue, ConditionableBuilder, Expression,
7    FilterableBuilder, KeyConditionableBuilder, fmt_attr_maps, resolve_expression,
8    utils::resolve_attr_path,
9};
10
11/// Comparison operators for DynamoDB condition and filter expressions.
12///
13/// Used with [`Condition::cmp`] and [`Condition::size_cmp`] to build
14/// comparison expressions. The convenience constructors generally cover
15/// the common cases without needing to name this enum directly.
16///
17/// # Examples
18///
19/// ```
20/// use dynamodb_facade::{Comparison, Condition};
21///
22/// // Using the enum directly with cmp():
23/// let cond = Condition::cmp("progress", Comparison::Ge, 0.5);
24/// assert_eq!(format!("{cond}"), r#"progress >= N("0.5")"#);
25///
26/// // Equivalent shorthand:
27/// let cond = Condition::ge("progress", 0.5);
28/// assert_eq!(format!("{cond}"), r#"progress >= N("0.5")"#);
29/// ```
30#[derive(Debug, Clone, Copy)]
31pub enum Comparison {
32    /// Equal (`=`).
33    Eq,
34
35    /// Not equal (`<>`).
36    Neq,
37
38    /// Less than (`<`).
39    Lt,
40
41    /// Less than or equal (`<=`).
42    Le,
43
44    /// Greater than (`>`).
45    Gt,
46
47    /// Greater than or equal (`>=`).
48    Ge,
49}
50
51impl fmt::Display for Comparison {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.write_str(match self {
54            Comparison::Eq => "=",
55            Comparison::Neq => "<>",
56            Comparison::Lt => "<",
57            Comparison::Le => "<=",
58            Comparison::Gt => ">",
59            Comparison::Ge => ">=",
60        })
61    }
62}
63
64// ---------------------------------------------------------------------------
65// Composable Condition type
66// ---------------------------------------------------------------------------
67
68#[derive(Debug, Clone)]
69enum ConditionInner<'a> {
70    // -- Logical combinators --------------------------------------------------
71    And(Vec<Condition<'a>>),
72
73    Or(Vec<Condition<'a>>),
74
75    Not(Box<Condition<'a>>),
76
77    // -- Comparisons ----------------------------------------------------------
78    Compare {
79        attr: Cow<'a, str>,
80        cmp: Comparison,
81        value: AttributeValue,
82    },
83
84    // -- Range / set operators ------------------------------------------------
85    Between {
86        attr: Cow<'a, str>,
87        low: AttributeValue,
88        high: AttributeValue,
89    },
90
91    In {
92        attr: Cow<'a, str>,
93        values: Vec<AttributeValue>,
94    },
95
96    // -- Functions ------------------------------------------------------------
97    Exists(Cow<'a, str>),
98
99    NotExists(Cow<'a, str>),
100
101    BeginsWith {
102        attr: Cow<'a, str>,
103        prefix: AttributeValue,
104    },
105
106    Contains {
107        attr: Cow<'a, str>,
108        value: AttributeValue,
109    },
110
111    // -- Size -----------------------------------------------------------------
112    SizeCompare {
113        attr: Cow<'a, str>,
114        cmp: Comparison,
115        value: AttributeValue,
116    },
117}
118
119/// Composable DynamoDB condition expression builder.
120///
121/// `Condition<'a>` represents a single DynamoDB condition expression that can
122/// be used as a `ConditionExpression`, or `FilterExpression`. Conditions are
123/// built from static constructor methods and composed with the `&`
124/// ([`BitAnd`](core::ops::BitAnd)), `|` ([`BitOr`](core::ops::BitOr)), and `!`
125/// ([`Not`](core::ops::Not)) operators, or with the variadic
126/// [`and`](Condition::and) and [`or`](Condition::or) methods.
127///
128/// Attribute names that are DynamoDB reserved words are automatically escaped
129/// with `#` expression attribute name placeholders. You never need to manage
130/// placeholder names manually.
131///
132/// # Display
133///
134/// `Condition` implements [`Display`](core::fmt::Display) in two modes:
135///
136/// - **Default (`{}`)** — resolves all placeholders inline for human-readable
137///   debugging: `PK = S("USER#user-1")`
138/// - **Alternate (`{:#}`)** — shows the raw expression with `#name` / `:value`
139///   placeholders and separate name/value maps, matching what DynamoDB receives:
140///   `PK = :c0\n  values: { :c0 = S("USER#user-1") }`
141///
142/// # Examples
143///
144/// Simple equality condition:
145///
146/// ```
147/// use dynamodb_facade::Condition;
148///
149/// let cond = Condition::eq("role", "instructor");
150/// assert_eq!(format!("{cond}"), r#"role = S("instructor")"#);
151/// ```
152///
153/// Composing conditions with operators:
154///
155/// ```
156/// use dynamodb_facade::Condition;
157///
158/// let cond = Condition::exists("email")
159///     & !Condition::eq("role", "banned");
160/// assert_eq!(
161///     format!("{cond}"),
162///     r#"(attribute_exists(email) AND (NOT role = S("banned")))"#
163/// );
164/// ```
165///
166/// Variadic composition:
167///
168/// ```
169/// use dynamodb_facade::{Comparison, Condition};
170///
171/// let cond = Condition::and([
172///     Condition::eq("role", "student"),
173///     Condition::size_gt("tags", 0),
174///     Condition::exists("enrolled_at"),
175/// ]);
176/// assert_eq!(
177///     format!("{cond}"),
178///     r#"(role = S("student") AND size(tags) > N("0") AND attribute_exists(enrolled_at))"#
179/// );
180/// ```
181#[derive(Debug, Clone)]
182#[must_use = "condition does nothing until applied to a request"]
183pub struct Condition<'a>(ConditionInner<'a>);
184
185// -- Constructor methods ------------------------------------------------------
186
187impl<'a> Condition<'a> {
188    // Comparisons
189
190    /// Creates a condition that compares an attribute to a value using the given operator.
191    ///
192    /// This is the general form underlying the convenience methods [`eq`](Condition::eq),
193    /// [`ne`](Condition::ne), [`lt`](Condition::lt), [`le`](Condition::le),
194    /// [`gt`](Condition::gt), and [`ge`](Condition::ge).
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use dynamodb_facade::{Comparison, Condition};
200    ///
201    /// let cond = Condition::cmp("progress", Comparison::Ge, 0.75);
202    /// assert_eq!(format!("{cond}"), r#"progress >= N("0.75")"#);
203    /// ```
204    pub fn cmp(
205        attr: impl Into<Cow<'a, str>>,
206        cmp: Comparison,
207        value: impl IntoAttributeValue,
208    ) -> Self {
209        Self(ConditionInner::Compare {
210            attr: attr.into(),
211            cmp,
212            value: value.into_attribute_value(),
213        })
214    }
215
216    /// Creates an equality condition: `attr = value`.
217    ///
218    /// # Examples
219    ///
220    /// ```
221    /// use dynamodb_facade::Condition;
222    ///
223    /// let cond = Condition::eq("role", "instructor");
224    /// assert_eq!(format!("{cond}"), r#"role = S("instructor")"#);
225    /// ```
226    pub fn eq(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
227        Self::cmp(attr, Comparison::Eq, value)
228    }
229
230    /// Creates a not-equal condition: `attr <> value`.
231    ///
232    /// # Examples
233    ///
234    /// ```
235    /// use dynamodb_facade::Condition;
236    ///
237    /// let cond = Condition::ne("role", "banned");
238    /// assert_eq!(format!("{cond}"), r#"role <> S("banned")"#);
239    /// ```
240    pub fn ne(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
241        Self::cmp(attr, Comparison::Neq, value)
242    }
243
244    /// Creates a less-than condition: `attr < value`.
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// use dynamodb_facade::Condition;
250    ///
251    /// let cond = Condition::lt("progress", 1.0);
252    /// assert_eq!(format!("{cond}"), r#"progress < N("1")"#);
253    /// ```
254    pub fn lt(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
255        Self::cmp(attr, Comparison::Lt, value)
256    }
257
258    /// Creates a less-than-or-equal condition: `attr <= value`.
259    ///
260    /// # Examples
261    ///
262    /// ```
263    /// use dynamodb_facade::Condition;
264    ///
265    /// let cond = Condition::le("max_enrollments", 100);
266    /// assert_eq!(format!("{cond}"), r#"max_enrollments <= N("100")"#);
267    /// ```
268    pub fn le(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
269        Self::cmp(attr, Comparison::Le, value)
270    }
271
272    /// Creates a greater-than condition: `attr > value`.
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use dynamodb_facade::Condition;
278    ///
279    /// let cond = Condition::gt("enrolled_at", 0);
280    /// assert_eq!(format!("{cond}"), r#"enrolled_at > N("0")"#);
281    /// ```
282    pub fn gt(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
283        Self::cmp(attr, Comparison::Gt, value)
284    }
285
286    /// Creates a greater-than-or-equal condition: `attr >= value`.
287    ///
288    /// # Examples
289    ///
290    /// ```
291    /// use dynamodb_facade::Condition;
292    ///
293    /// let cond = Condition::ge("progress", 0.5);
294    /// assert_eq!(format!("{cond}"), r#"progress >= N("0.5")"#);
295    /// ```
296    pub fn ge(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
297        Self::cmp(attr, Comparison::Ge, value)
298    }
299
300    // Range / set
301
302    /// Creates a range condition: `attr BETWEEN low AND high` (inclusive).
303    ///
304    /// # Examples
305    ///
306    /// ```
307    /// use dynamodb_facade::Condition;
308    ///
309    /// let cond = Condition::between("enrolled_at", 1_700_000_000, 1_800_000_000);
310    /// assert_eq!(
311    ///     format!("{cond}"),
312    ///     r#"enrolled_at BETWEEN N("1700000000") AND N("1800000000")"#
313    /// );
314    /// ```
315    pub fn between(
316        attr: impl Into<Cow<'a, str>>,
317        low: impl IntoAttributeValue,
318        high: impl IntoAttributeValue,
319    ) -> Self {
320        Self(ConditionInner::Between {
321            attr: attr.into(),
322
323            low: low.into_attribute_value(),
324            high: high.into_attribute_value(),
325        })
326    }
327
328    /// Creates a membership condition: `attr IN (val1, val2, ...)`.
329    ///
330    /// # Examples
331    ///
332    /// ```
333    /// use dynamodb_facade::Condition;
334    ///
335    /// let cond = Condition::is_in("role", ["student", "instructor"]);
336    /// assert_eq!(
337    ///     format!("{cond}"),
338    ///     r#"role IN (S("student"), S("instructor"))"#
339    /// );
340    /// ```
341    pub fn is_in(
342        attr: impl Into<Cow<'a, str>>,
343        values: impl IntoIterator<Item = impl IntoAttributeValue>,
344    ) -> Self {
345        Self(ConditionInner::In {
346            attr: attr.into(),
347            values: values
348                .into_iter()
349                .map(IntoAttributeValue::into_attribute_value)
350                .collect(),
351        })
352    }
353
354    // Functions
355
356    /// Creates an attribute-existence condition: `attribute_exists(attr)`.
357    ///
358    /// Evaluates to true when the named attribute is present on the item,
359    /// regardless of its value.
360    ///
361    /// # Examples
362    ///
363    /// ```
364    /// use dynamodb_facade::Condition;
365    ///
366    /// let cond = Condition::exists("email");
367    /// assert_eq!(format!("{cond}"), "attribute_exists(email)");
368    /// ```
369    pub fn exists(attr: impl Into<Cow<'a, str>>) -> Self {
370        Self(ConditionInner::Exists(attr.into()))
371    }
372
373    /// Creates an attribute-absence condition: `attribute_not_exists(attr)`.
374    ///
375    /// Evaluates to true when the named attribute is absent from the item.
376    /// Commonly used to implement create-only semantics (e.g. "put only if
377    /// item does not exist").
378    ///
379    /// # Examples
380    ///
381    /// ```
382    /// use dynamodb_facade::Condition;
383    ///
384    /// let cond = Condition::not_exists("deleted_at");
385    /// assert_eq!(format!("{cond}"), "attribute_not_exists(deleted_at)");
386    /// ```
387    pub fn not_exists(attr: impl Into<Cow<'a, str>>) -> Self {
388        Self(ConditionInner::NotExists(attr.into()))
389    }
390
391    /// Creates a prefix condition: `begins_with(attr, prefix)`.
392    ///
393    /// Evaluates to true when the string attribute starts with the given prefix.
394    ///
395    /// # Examples
396    ///
397    /// ```
398    /// use dynamodb_facade::Condition;
399    ///
400    /// let cond = Condition::begins_with("SK", "ENROLL#");
401    /// assert_eq!(format!("{cond}"), r#"begins_with(SK, S("ENROLL#"))"#);
402    /// ```
403    pub fn begins_with(attr: impl Into<Cow<'a, str>>, prefix: impl IntoAttributeValue) -> Self {
404        Self(ConditionInner::BeginsWith {
405            attr: attr.into(),
406            prefix: prefix.into_attribute_value(),
407        })
408    }
409
410    /// Creates a containment condition: `contains(attr, value)`.
411    ///
412    /// For string attributes, evaluates to true when the attribute contains
413    /// `value` as a substring. For set attributes, evaluates to true when the
414    /// set contains `value` as an element.
415    ///
416    /// # Examples
417    ///
418    /// ```
419    /// use dynamodb_facade::Condition;
420    ///
421    /// let cond = Condition::contains("tags", "rust");
422    /// assert_eq!(format!("{cond}"), r#"contains(tags, S("rust"))"#);
423    /// ```
424    pub fn contains(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
425        Self(ConditionInner::Contains {
426            attr: attr.into(),
427            value: value.into_attribute_value(),
428        })
429    }
430
431    // Size
432
433    /// Creates a size comparison condition: `size(attr) <op> value`.
434    ///
435    /// Compares the size of an attribute (string length, list/map/set
436    /// cardinality, or binary length) to a [`usize`] using the given
437    /// [`Comparison`] operator. This is the general form underlying
438    /// the convenience methods [`size_eq`](Condition::size_eq),
439    /// [`size_ne`](Condition::size_ne), [`size_lt`](Condition::size_lt),
440    /// [`size_le`](Condition::size_le), [`size_gt`](Condition::size_gt), and
441    /// [`size_ge`](Condition::size_ge).
442    ///
443    /// # Examples
444    ///
445    /// ```
446    /// use dynamodb_facade::{Comparison, Condition};
447    ///
448    /// let cond = Condition::size_cmp("tags", Comparison::Ge, 1);
449    /// assert_eq!(format!("{cond}"), r#"size(tags) >= N("1")"#);
450    /// ```
451    pub fn size_cmp(attr: impl Into<Cow<'a, str>>, cmp: Comparison, value: usize) -> Self {
452        Self(ConditionInner::SizeCompare {
453            attr: attr.into(),
454            cmp,
455            value: value.into_attribute_value(),
456        })
457    }
458
459    /// Creates a size-equal condition: `size(attr) = value`.
460    ///
461    /// # Examples
462    ///
463    /// ```
464    /// use dynamodb_facade::Condition;
465    ///
466    /// let cond = Condition::size_eq("tags", 3);
467    /// assert_eq!(format!("{cond}"), r#"size(tags) = N("3")"#);
468    /// ```
469    pub fn size_eq(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
470        Self::size_cmp(attr, Comparison::Eq, value)
471    }
472
473    /// Creates a size-not-equal condition: `size(attr) <> value`.
474    ///
475    /// # Examples
476    ///
477    /// ```
478    /// use dynamodb_facade::Condition;
479    ///
480    /// let cond = Condition::size_ne("tags", 0);
481    /// assert_eq!(format!("{cond}"), r#"size(tags) <> N("0")"#);
482    /// ```
483    pub fn size_ne(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
484        Self::size_cmp(attr, Comparison::Neq, value)
485    }
486
487    /// Creates a size-less-than condition: `size(attr) < value`.
488    ///
489    /// # Examples
490    ///
491    /// ```
492    /// use dynamodb_facade::Condition;
493    ///
494    /// let cond = Condition::size_lt("content", 1000);
495    /// assert_eq!(format!("{cond}"), r#"size(content) < N("1000")"#);
496    /// ```
497    pub fn size_lt(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
498        Self::size_cmp(attr, Comparison::Lt, value)
499    }
500
501    /// Creates a size-less-than-or-equal condition: `size(attr) <= value`.
502    ///
503    /// # Examples
504    ///
505    /// ```
506    /// use dynamodb_facade::Condition;
507    ///
508    /// let cond = Condition::size_le("bio", 500);
509    /// assert_eq!(format!("{cond}"), r#"size(bio) <= N("500")"#);
510    /// ```
511    pub fn size_le(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
512        Self::size_cmp(attr, Comparison::Le, value)
513    }
514
515    /// Creates a size-greater-than condition: `size(attr) > value`.
516    ///
517    /// # Examples
518    ///
519    /// ```
520    /// use dynamodb_facade::Condition;
521    ///
522    /// let cond = Condition::size_gt("tags", 0);
523    /// assert_eq!(format!("{cond}"), r#"size(tags) > N("0")"#);
524    /// ```
525    pub fn size_gt(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
526        Self::size_cmp(attr, Comparison::Gt, value)
527    }
528    /// Creates a size-greater-than-or-equal condition: `size(attr) >= value`.
529    ///
530    /// # Examples
531    ///
532    /// ```
533    /// use dynamodb_facade::Condition;
534    ///
535    /// let cond = Condition::size_ge("email", 5);
536    /// assert_eq!(format!("{cond}"), r#"size(email) >= N("5")"#);
537    /// ```
538    pub fn size_ge(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
539        Self::size_cmp(attr, Comparison::Ge, value)
540    }
541
542    // Logical combinators
543
544    /// Creates a condition that is true when **all** of the given conditions are true.
545    ///
546    /// Nested `And` conditions are flattened automatically. An empty iterator
547    /// produces a no-op condition that renders as `<none>` and is silently
548    /// dropped when applied to a builder.
549    ///
550    /// For combining a fixed number of conditions, the `&` operator is more ergonomic.
551    ///
552    /// # Examples
553    ///
554    /// ```
555    /// use dynamodb_facade::Condition;
556    ///
557    /// let cond = Condition::and([
558    ///     Condition::exists("email"),
559    ///     Condition::eq("role", "student"),
560    ///     Condition::gt("enrolled_at", 0),
561    /// ]);
562    /// assert_eq!(
563    ///     format!("{cond}"),
564    ///     r#"(attribute_exists(email) AND role = S("student") AND enrolled_at > N("0"))"#
565    /// );
566    /// ```
567    ///
568    /// Empty iterator produces a no-op:
569    ///
570    /// ```
571    /// use dynamodb_facade::Condition;
572    ///
573    /// let cond = Condition::and([] as [Condition; 0]);
574    /// assert_eq!(format!("{cond}"), "<none>");
575    /// ```
576    ///
577    /// The & operator produce the same result:
578    ///
579    /// ```
580    /// use dynamodb_facade::Condition;
581    ///
582    /// let cond1 = Condition::and([
583    ///     Condition::exists("email"),
584    ///     Condition::eq("role", "student"),
585    ///     Condition::gt("enrolled_at", 0),
586    /// ]);
587    ///
588    /// let cond2 = Condition::exists("email")
589    ///     & Condition::eq("role", "student")
590    ///     & Condition::gt("enrolled_at", 0);
591    /// assert_eq!(format!("{cond1}"), format!("{cond2}"));
592    /// ```
593    pub fn and(conditions: impl IntoIterator<Item = Condition<'a>>) -> Self {
594        let iterator = conditions.into_iter();
595        let est_size = iterator.size_hint().0;
596        Self(ConditionInner::And(iterator.fold(
597            Vec::with_capacity(est_size),
598            |mut conditions, c| {
599                match c.0 {
600                    ConditionInner::And(conds) => {
601                        conditions.extend(conds);
602                    }
603                    _ => {
604                        conditions.push(c);
605                    }
606                };
607                conditions
608            },
609        )))
610    }
611
612    /// Creates a condition that is true when **any** of the given conditions is true.
613    ///
614    /// Nested `Or` conditions are flattened automatically. An empty iterator
615    /// produces a no-op condition that renders as `<none>` and is silently
616    /// dropped when applied to a builder.
617    ///
618    /// For combining a fixed number of conditions, the `|` operator is more ergonomic.
619    ///
620    /// # Examples
621    ///
622    /// ```
623    /// use dynamodb_facade::Condition;
624    ///
625    /// let cond = Condition::or([
626    ///     Condition::not_exists("deleted_at"),
627    ///     Condition::eq("role", "admin"),
628    /// ]);
629    /// assert_eq!(
630    ///     format!("{cond}"),
631    ///     r#"(attribute_not_exists(deleted_at) OR role = S("admin"))"#
632    /// );
633    /// ```
634    ///
635    /// Empty iterator produces a no-op:
636    ///
637    /// ```
638    /// use dynamodb_facade::Condition;
639    ///
640    /// let cond = Condition::or([] as [Condition; 0]);
641    /// assert_eq!(format!("{cond}"), "<none>");
642    /// ```
643    ///
644    /// The | operator produce the same result:
645    ///
646    /// ```
647    /// use dynamodb_facade::Condition;
648    ///
649    /// let cond1 = Condition::or([
650    ///     Condition::not_exists("deleted_at"),
651    ///     Condition::eq("role", "admin"),
652    /// ]);
653    ///
654    /// let cond2 = Condition::not_exists("deleted_at")
655    ///     | Condition::eq("role", "admin");
656    /// assert_eq!(format!("{cond1}"), format!("{cond2}"));
657    /// ```
658    pub fn or(conditions: impl IntoIterator<Item = Condition<'a>>) -> Self {
659        let iterator = conditions.into_iter();
660        let est_size = iterator.size_hint().0;
661        Self(ConditionInner::Or(iterator.fold(
662            Vec::with_capacity(est_size),
663            |mut conditions, c| {
664                match c.0 {
665                    ConditionInner::Or(conds) => {
666                        conditions.extend(conds);
667                    }
668                    _ => {
669                        conditions.push(c);
670                    }
671                };
672                conditions
673            },
674        )))
675    }
676}
677
678/// Negates a condition: `NOT cond`.
679///
680/// Applying `!` twice cancels out — `!!cond` returns the original condition.
681///
682/// # Examples
683///
684/// ```
685/// use dynamodb_facade::Condition;
686///
687/// let cond = !Condition::eq("role", "banned");
688/// assert_eq!(format!("{cond}"), r#"(NOT role = S("banned"))"#);
689///
690/// // Double negation cancels out.
691/// let cond = !!Condition::exists("email");
692/// assert_eq!(format!("{cond}"), "attribute_exists(email)");
693/// ```
694impl<'a> core::ops::Not for Condition<'a> {
695    type Output = Condition<'a>;
696
697    fn not(self) -> Self::Output {
698        match self.0 {
699            ConditionInner::Not(condition) => *condition,
700            _ => Self(ConditionInner::Not(Box::new(self))),
701        }
702    }
703}
704
705/// Combines two conditions with AND: `lhs & rhs`.
706///
707/// Equivalent to `Condition::and([lhs, rhs])`. Nested `And` conditions are
708/// flattened automatically.
709///
710/// # Examples
711///
712/// ```
713/// use dynamodb_facade::Condition;
714///
715/// let cond = Condition::exists("email") & Condition::eq("role", "student");
716/// assert!(format!("{cond}").contains("AND"));
717/// assert_eq!(
718///     format!("{cond}"),
719///     r#"(attribute_exists(email) AND role = S("student"))"#
720/// );
721/// ```
722impl<'a> core::ops::BitAnd for Condition<'a> {
723    type Output = Condition<'a>;
724
725    fn bitand(self, rhs: Self) -> Self::Output {
726        Self::and([self, rhs])
727    }
728}
729
730/// Combines two conditions with OR: `lhs | rhs`.
731///
732/// Equivalent to `Condition::or([lhs, rhs])`. Nested `Or` conditions are
733/// flattened automatically.
734///
735/// # Examples
736///
737/// ```
738/// use dynamodb_facade::Condition;
739///
740/// let cond = Condition::not_exists("deleted_at") | Condition::eq("role", "admin");
741/// assert_eq!(
742///     format!("{cond}"),
743///     r#"(attribute_not_exists(deleted_at) OR role = S("admin"))"#
744/// );
745/// ```
746impl<'a> core::ops::BitOr for Condition<'a> {
747    type Output = Condition<'a>;
748
749    fn bitor(self, rhs: Self) -> Self::Output {
750        Self::or([self, rhs])
751    }
752}
753
754// -- Internal build machinery -------------------------------------------------
755
756#[derive(Debug, Default)]
757struct BuiltCondition {
758    expression: Expression,
759    names: AttrNames,
760    values: AttrValues,
761}
762impl BuiltCondition {
763    const EMPTY: Self = Self {
764        expression: String::new(),
765        names: vec![],
766        values: vec![],
767    };
768}
769
770impl Condition<'_> {
771    /// Builds the condition starting the placeholder counter at zero.
772    fn build(self) -> BuiltCondition {
773        self.build_with_counter(&mut 0)
774    }
775
776    /// Builds the condition using a shared counter for unique placeholder names.
777    fn build_with_counter(self, counter: &mut usize) -> BuiltCondition {
778        match self.0 {
779            ConditionInner::And(conditions) => Self::build_logical(conditions, " AND ", counter),
780            ConditionInner::Or(conditions) => Self::build_logical(conditions, " OR ", counter),
781
782            ConditionInner::Not(inner) => {
783                let mut built = inner.build_with_counter(counter);
784                if !built.expression.is_empty() {
785                    built.expression = format!("(NOT {})", built.expression);
786                }
787                built
788            }
789
790            ConditionInner::Compare { attr, cmp, value } => {
791                let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
792                let val_id = *counter;
793                *counter += 1;
794                let val_ph = format!(":c{val_id}");
795                BuiltCondition {
796                    expression: format!("{attr_expr} {cmp} {val_ph}"),
797                    names,
798                    values: vec![(val_ph, value)],
799                }
800            }
801
802            ConditionInner::Between { attr, low, high } => {
803                let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
804                let val_id = *counter;
805                *counter += 1;
806                let lo_ph = format!(":c{val_id}lo");
807                let hi_ph = format!(":c{val_id}hi");
808                BuiltCondition {
809                    expression: format!("{attr_expr} BETWEEN {lo_ph} AND {hi_ph}"),
810                    names,
811                    values: vec![(lo_ph, low), (hi_ph, high)],
812                }
813            }
814
815            ConditionInner::In { attr, values } => {
816                if values.is_empty() {
817                    BuiltCondition::EMPTY
818                } else {
819                    let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
820                    let val_id = *counter;
821                    *counter += 1;
822                    let val_phs: Vec<String> = (0..values.len())
823                        .map(|i| format!(":c{val_id}i{i}"))
824                        .collect();
825                    let in_list = val_phs.join(", ");
826                    BuiltCondition {
827                        expression: format!("{attr_expr} IN ({in_list})"),
828                        names,
829                        values: val_phs.into_iter().zip(values.iter().cloned()).collect(),
830                    }
831                }
832            }
833
834            ConditionInner::Exists(attr) => {
835                let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
836                BuiltCondition {
837                    expression: format!("attribute_exists({attr_expr})"),
838                    names,
839                    values: vec![],
840                }
841            }
842
843            ConditionInner::NotExists(attr) => {
844                let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
845                BuiltCondition {
846                    expression: format!("attribute_not_exists({attr_expr})"),
847                    names,
848                    values: vec![],
849                }
850            }
851
852            ConditionInner::BeginsWith { attr, prefix } => {
853                let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
854                let val_id = *counter;
855                *counter += 1;
856                let prefix_ph = format!(":c{val_id}");
857                BuiltCondition {
858                    expression: format!("begins_with({attr_expr}, {prefix_ph})"),
859                    names,
860                    values: vec![(prefix_ph, prefix)],
861                }
862            }
863
864            ConditionInner::Contains { attr, value } => {
865                let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
866                let val_id = *counter;
867                *counter += 1;
868                let val_ph = format!(":c{val_id}");
869                BuiltCondition {
870                    expression: format!("contains({attr_expr}, {val_ph})"),
871                    names,
872                    values: vec![(val_ph, value)],
873                }
874            }
875
876            ConditionInner::SizeCompare { attr, cmp, value } => {
877                let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
878                let val_id = *counter;
879                *counter += 1;
880                let val_ph = format!(":c{val_id}");
881                BuiltCondition {
882                    expression: format!("size({attr_expr}) {cmp} {val_ph}"),
883                    names,
884                    values: vec![(val_ph, value)],
885                }
886            }
887        }
888    }
889
890    /// Builds a logical AND/OR expression by joining sub-conditions with `operator`.
891    fn build_logical(
892        conditions: Vec<Condition>,
893        operator: &str,
894        counter: &mut usize,
895    ) -> BuiltCondition {
896        let mut result = BuiltCondition::default();
897        let mut parts = Vec::with_capacity(conditions.len());
898
899        for cond in conditions {
900            let BuiltCondition {
901                expression,
902                names,
903                values,
904            } = cond.build_with_counter(counter);
905            if !expression.is_empty() {
906                parts.push(expression);
907                result.names.extend(names);
908                result.values.extend(values);
909            }
910        }
911
912        result.expression = match parts.len() {
913            0 => String::new(),
914            1 => parts.pop().expect("parts has exactly one element"),
915            _ => {
916                let joined = parts.join(operator);
917                format!("({joined})")
918            }
919        };
920
921        result
922    }
923}
924
925// -- Display ------------------------------------------------------------------
926
927/// Formats the condition for display.
928///
929/// Two modes are supported:
930///
931/// - **Default (`{}`)** — resolves all `#name` and `:value` placeholders
932///   inline, producing a human-readable string useful for debugging:
933///   `PK = S("USER#user-1")`
934/// - **Alternate (`{:#}`)** — shows the raw DynamoDB expression with
935///   placeholder names, followed by the name and value maps on separate lines.
936///   This matches what is actually sent to DynamoDB:
937///   `PK = :c0\n  values: { :c0 = S("USER#user-1") }`
938///
939/// An empty condition (e.g. `Condition::and([])`) renders as `<none>` in both
940/// modes.
941///
942/// # Examples
943///
944/// ```
945/// use dynamodb_facade::Condition;
946///
947/// let cond = Condition::eq("PK", "USER#user-1");
948///
949/// // Default: placeholders resolved inline.
950/// assert_eq!(format!("{cond}"), r#"PK = S("USER#user-1")"#);
951///
952/// // Alternate: raw expression + maps.
953/// assert_eq!(format!("{cond:#}"), "PK = :c0\n  values: { :c0 = S(\"USER#user-1\") }");
954/// ```
955impl fmt::Display for Condition<'_> {
956    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
957        let built = self.clone().build();
958        if built.expression.is_empty() {
959            return f.write_str("<none>");
960        }
961        if f.alternate() {
962            f.write_str(&built.expression)?;
963            fmt_attr_maps(f, &built.names, &built.values)
964        } else {
965            f.write_str(&resolve_expression(
966                &built.expression,
967                &built.names,
968                &built.values,
969            ))
970        }
971    }
972}
973
974// -- ApplyCondition impl (condition_expression) -------------------------------
975
976impl<B: ConditionableBuilder> ApplyCondition<B> for Condition<'_> {
977    fn apply(self, builder: B) -> B {
978        let built = self.build();
979        if built.expression.is_empty() {
980            return builder;
981        }
982        builder
983            .condition_expression(built.expression)
984            .apply_names_and_values(built.names, built.values)
985    }
986}
987
988impl<B: ConditionableBuilder> ApplyCondition<B> for Option<Condition<'_>> {
989    fn apply(self, builder: B) -> B {
990        match self {
991            Some(c) => c.apply(builder),
992            None => builder,
993        }
994    }
995}
996
997// -- ApplyKeyCondition impl (key_condition_expression) ------------------------
998
999impl<B: KeyConditionableBuilder> ApplyKeyCondition<B> for Condition<'_> {
1000    fn apply_key_condition(self, builder: B) -> B {
1001        let built = self.build();
1002        if built.expression.is_empty() {
1003            return builder;
1004        }
1005        builder
1006            .key_condition_expression(built.expression)
1007            .apply_names_and_values(built.names, built.values)
1008    }
1009}
1010
1011// -- ApplyFilter impl (filter_expression) -------------------------------------
1012
1013impl<B: FilterableBuilder> ApplyFilter<B> for Condition<'_> {
1014    fn apply_filter(self, builder: B) -> B {
1015        let built = self.build();
1016        if built.expression.is_empty() {
1017            return builder;
1018        }
1019        builder
1020            .filter_expression(built.expression)
1021            .apply_names_and_values(built.names, built.values)
1022    }
1023}
1024
1025impl<B: FilterableBuilder> ApplyFilter<B> for Option<Condition<'_>> {
1026    fn apply_filter(self, builder: B) -> B {
1027        match self {
1028            Some(c) => c.apply_filter(builder),
1029            None => builder,
1030        }
1031    }
1032}
1033
1034#[cfg(test)]
1035mod tests {
1036    use super::*;
1037
1038    #[test]
1039    fn test_condition_display_default_simple_eq() {
1040        let c = Condition::eq("PK", "USER#123");
1041        let display = format!("{c}");
1042        assert_eq!(display, r#"PK = S("USER#123")"#);
1043    }
1044
1045    #[test]
1046    fn test_condition_display_default_reserved_word() {
1047        // "Status" is a reserved word → gets a #c0 placeholder internally
1048        let c = Condition::eq("Status", "active");
1049        let display = format!("{c}");
1050        assert_eq!(display, r#"Status = S("active")"#);
1051    }
1052
1053    #[test]
1054    fn test_condition_display_default_and_with_begins_with() {
1055        let c = Condition::and([
1056            Condition::eq("PK", "USER#123"),
1057            Condition::begins_with("SK", "ORDER#"),
1058        ]);
1059        let display = format!("{c}");
1060        assert_eq!(
1061            display,
1062            r#"(PK = S("USER#123") AND begins_with(SK, S("ORDER#")))"#
1063        );
1064    }
1065
1066    #[test]
1067    fn test_condition_display_default_or() {
1068        let c = Condition::or([
1069            Condition::eq("attr1", "value1"),
1070            Condition::ne("attr2", 2345u32),
1071        ]);
1072        let display = format!("{c}");
1073        assert_eq!(display, r#"(attr1 = S("value1") OR attr2 <> N("2345"))"#);
1074    }
1075
1076    #[test]
1077    fn test_condition_display_default_not() {
1078        let c = !Condition::exists("PK");
1079        let display = format!("{c}");
1080        assert_eq!(display, "(NOT attribute_exists(PK))");
1081    }
1082
1083    #[test]
1084    fn test_condition_display_default_between() {
1085        let c = Condition::between("age", 18u32, 65u32);
1086        let display = format!("{c}");
1087        assert_eq!(display, r#"age BETWEEN N("18") AND N("65")"#);
1088    }
1089
1090    #[test]
1091    fn test_condition_display_default_in() {
1092        let c = Condition::is_in("color", ["red", "green", "blue"]);
1093        let display = format!("{c}");
1094        assert_eq!(display, r#"color IN (S("red"), S("green"), S("blue"))"#);
1095    }
1096
1097    #[test]
1098    fn test_condition_display_default_size() {
1099        let c = Condition::size_cmp("tags", Comparison::Ge, 3);
1100        let display = format!("{c}");
1101        assert_eq!(display, r#"size(tags) >= N("3")"#);
1102    }
1103
1104    #[test]
1105    fn test_condition_display_default_contains() {
1106        let c = Condition::contains("description", "rust");
1107        let display = format!("{c}");
1108        assert_eq!(display, r#"contains(description, S("rust"))"#);
1109    }
1110
1111    #[test]
1112    fn test_condition_display_default_empty() {
1113        let c = Condition::and([]);
1114        let display = format!("{c}");
1115        assert_eq!(display, "<none>");
1116    }
1117
1118    #[test]
1119    fn test_condition_display_alternate_simple() {
1120        let c = Condition::eq("PK", "USER#123");
1121        let display = format!("{c:#}");
1122        assert_eq!(display, "PK = :c0\n  values: { :c0 = S(\"USER#123\") }");
1123    }
1124
1125    #[test]
1126    fn test_condition_display_alternate_reserved_word() {
1127        let c = Condition::eq("Status", "active");
1128        let display = format!("{c:#}");
1129        assert_eq!(
1130            display,
1131            "#c0 = :c1\n  names: { #c0 = Status }\n  values: { :c1 = S(\"active\") }"
1132        );
1133    }
1134
1135    #[test]
1136    fn test_condition_display_alternate_and_with_begins_with() {
1137        let c = Condition::and([
1138            Condition::eq("PK", "USER#123"),
1139            Condition::begins_with("SK", "ORDER#"),
1140        ]);
1141        let display = format!("{c:#}");
1142        assert_eq!(
1143            display,
1144            "(PK = :c0 AND begins_with(SK, :c1))\n  values: { :c0 = S(\"USER#123\"), :c1 = S(\"ORDER#\") }"
1145        );
1146    }
1147
1148    #[test]
1149    fn test_condition_display_alternate_no_values() {
1150        let c = Condition::exists("PK");
1151        let display = format!("{c:#}");
1152        // No names (PK is not reserved) and no values
1153        assert_eq!(display, "attribute_exists(PK)");
1154    }
1155
1156    #[test]
1157    fn test_condition_display_alternate_empty() {
1158        let c = Condition::and([]);
1159        let display = format!("{c:#}");
1160        assert_eq!(display, "<none>");
1161    }
1162}