Skip to main content

dynamodb_expression/update/set/
math.rs

1use core::fmt::{self, Write};
2
3use crate::{
4    path::Path,
5    update::Update,
6    value::{Num, ValueOrRef},
7};
8
9/// Represents a [DynamoDB math operation][1] used as a part of an update expression.
10///
11/// Prefer [`Path::math`] over this.
12///
13/// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Math {
16    pub(crate) dst: Path,
17    pub(crate) src: Option<Path>,
18    op: MathOp,
19    pub(crate) num: ValueOrRef,
20}
21
22/// A [math operation][1] to modify a field and assign the updated value
23/// to another (possibly different) field.
24///
25/// Prefer [`Path::math`] over this.
26///
27/// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement
28impl Math {
29    /// Prefer [`Path::math`] over this.
30    pub fn builder<T>(dst: T) -> Builder
31    where
32        T: Into<Path>,
33    {
34        Builder {
35            dst: dst.into(),
36            src: None,
37        }
38    }
39
40    /// Add an additional [`Update`] statement to this expression.
41    ///
42    /// ```
43    /// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
44    /// use dynamodb_expression::{Num, Path};
45    /// # use pretty_assertions::assert_eq;
46    ///
47    /// let set = "foo"
48    ///     .parse::<Path>()?
49    ///     .math()
50    ///     .add(1)
51    ///     .and("bar".parse::<Path>()?.set("a value"));
52    /// assert_eq!(r#"SET foo = foo + 1, bar = "a value""#, set.to_string());
53    /// #
54    /// # Ok(())
55    /// # }
56    /// ```
57    pub fn and<T>(self, other: T) -> Update
58    where
59        T: Into<Update>,
60    {
61        Update::from(self).and(other)
62    }
63}
64
65impl fmt::Display for Math {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        self.dst.fmt(f)?;
68        f.write_str(" = ")?;
69        // If no source field is specified, default to using the destination field.
70        self.src.as_ref().unwrap_or(&self.dst).fmt(f)?;
71        f.write_char(' ')?;
72        self.op.fmt(f)?;
73        f.write_char(' ')?;
74        self.num.fmt(f)
75    }
76}
77
78#[derive(Clone, Copy, PartialEq, Eq)]
79enum MathOp {
80    Add,
81    Sub,
82}
83
84impl fmt::Debug for MathOp {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        fmt::Display::fmt(self, f)
87    }
88}
89
90impl fmt::Display for MathOp {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.write_char(match self {
93            MathOp::Add => '+',
94            MathOp::Sub => '-',
95        })
96    }
97}
98
99/// Prefer [`Path::math`] over this.
100#[must_use = "Consume this `Builder` by using its `.add()` or `.sub()` methods"]
101#[derive(Debug, Clone)]
102pub struct Builder {
103    dst: Path,
104    src: Option<Path>,
105}
106
107impl Builder {
108    /// Sets the source field to read the initial value from.
109    /// Defaults to the destination field.
110    pub fn src<T>(mut self, src: T) -> Self
111    where
112        T: Into<Path>,
113    {
114        self.src = Some(src.into());
115
116        self
117    }
118
119    /// Sets addition as the operation to perform.
120    #[rustversion::attr(before(1.81), allow(clippy::should_implement_trait))]
121    #[rustversion::attr(
122        since(1.81),
123        expect(
124            clippy::should_implement_trait,
125            reason = "This is for the DynamoDB `+` operator, not the Rust `+` operator."
126        )
127    )]
128    pub fn add<T>(self, num: T) -> Math
129    where
130        T: Into<Num>,
131    {
132        self.with_op(MathOp::Add, num)
133    }
134
135    /// Sets subtraction as the operation to perform.
136    #[rustversion::attr(before(1.81), allow(clippy::should_implement_trait))]
137    #[rustversion::attr(
138        since(1.81),
139        expect(
140            clippy::should_implement_trait,
141            reason = "This is for the DynamoDB `-` operator, not the Rust `-` operator."
142        )
143    )]
144    pub fn sub<T>(self, num: T) -> Math
145    where
146        T: Into<Num>,
147    {
148        self.with_op(MathOp::Sub, num)
149    }
150
151    fn with_op<T>(self, op: MathOp, num: T) -> Math
152    where
153        T: Into<Num>,
154    {
155        let Self { dst, src } = self;
156
157        Math {
158            dst,
159            src,
160            op,
161            num: num.into().into(),
162        }
163    }
164}
165
166#[cfg(test)]
167mod test {
168    use pretty_assertions::assert_eq;
169
170    use crate::{
171        update::{Assign, Set, SetAction},
172        Num, Path,
173    };
174
175    use super::Math;
176
177    #[test]
178    fn and() -> Result<(), Box<dyn std::error::Error>> {
179        let math: Math = "foo".parse::<Path>()?.math().add(1);
180        let assign: Assign = "bar".parse::<Path>()?.set(Num::new(8));
181
182        // Should be able to concatenate anything that can be turned into a SetAction.
183
184        let combined = math.clone().and(assign.clone());
185        assert_eq!(r#"SET foo = foo + 1, bar = 8"#, combined.to_string());
186
187        // Should be able to concatenate a SetAction instance.
188
189        let combined = math.clone().and(SetAction::from(assign.clone()));
190        assert_eq!(r#"SET foo = foo + 1, bar = 8"#, combined.to_string());
191
192        // Should be able to concatenate a Set instance
193
194        let set: Set = [
195            SetAction::from(assign),
196            SetAction::from("baz".parse::<Path>()?.list_append().list(["d", "e", "f"])),
197        ]
198        .into_iter()
199        .collect();
200        let combined = math.clone().and(set);
201        assert_eq!(
202            r#"SET foo = foo + 1, bar = 8, baz = list_append(baz, ["d", "e", "f"])"#,
203            combined.to_string()
204        );
205
206        // Should be able to concatenate a Remove instance
207
208        let combined = math.clone().and("quux".parse::<Path>()?.remove());
209        assert_eq!(r#"SET foo = foo + 1 REMOVE quux"#, combined.to_string());
210
211        // Should be able to concatenate a SetRemove instance
212
213        let combined = math.and("quux".parse::<Path>()?.remove());
214        assert_eq!(r#"SET foo = foo + 1 REMOVE quux"#, combined.to_string());
215
216        Ok(())
217    }
218}