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    #[allow(clippy::should_implement_trait)]
121    pub fn add<T>(self, num: T) -> Math
122    where
123        T: Into<Num>,
124    {
125        self.with_op(MathOp::Add, num)
126    }
127
128    /// Sets subtraction as the operation to perform.
129    #[allow(clippy::should_implement_trait)]
130    pub fn sub<T>(self, num: T) -> Math
131    where
132        T: Into<Num>,
133    {
134        self.with_op(MathOp::Sub, num)
135    }
136
137    fn with_op<T>(self, op: MathOp, num: T) -> Math
138    where
139        T: Into<Num>,
140    {
141        let Self { dst, src } = self;
142
143        Math {
144            dst,
145            src,
146            op,
147            num: num.into().into(),
148        }
149    }
150}
151
152#[cfg(test)]
153mod test {
154    use pretty_assertions::assert_eq;
155
156    use crate::{
157        update::{Assign, Set, SetAction},
158        Num, Path,
159    };
160
161    use super::Math;
162
163    #[test]
164    fn and() -> Result<(), Box<dyn std::error::Error>> {
165        let math: Math = "foo".parse::<Path>()?.math().add(1);
166        let assign: Assign = "bar".parse::<Path>()?.set(Num::new(8));
167
168        // Should be able to concatenate anything that can be turned into a SetAction.
169
170        let combined = math.clone().and(assign.clone());
171        assert_eq!(r#"SET foo = foo + 1, bar = 8"#, combined.to_string());
172
173        // Should be able to concatenate a SetAction instance.
174
175        let combined = math.clone().and(SetAction::from(assign.clone()));
176        assert_eq!(r#"SET foo = foo + 1, bar = 8"#, combined.to_string());
177
178        // Should be able to concatenate a Set instance
179
180        let set: Set = [
181            SetAction::from(assign),
182            SetAction::from("baz".parse::<Path>()?.list_append().list(["d", "e", "f"])),
183        ]
184        .into_iter()
185        .collect();
186        let combined = math.clone().and(set);
187        assert_eq!(
188            r#"SET foo = foo + 1, bar = 8, baz = list_append(baz, ["d", "e", "f"])"#,
189            combined.to_string()
190        );
191
192        // Should be able to concatenate a Remove instance
193
194        let combined = math.clone().and("quux".parse::<Path>()?.remove());
195        assert_eq!(r#"SET foo = foo + 1 REMOVE quux"#, combined.to_string());
196
197        // Should be able to concatenate a SetRemove instance
198
199        let combined = math.and("quux".parse::<Path>()?.remove());
200        assert_eq!(r#"SET foo = foo + 1 REMOVE quux"#, combined.to_string());
201
202        Ok(())
203    }
204}