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}