dynamodb_expression/
key.rs

1//! Types related to [DynamoDB key condition expressions][1].
2//!
3//! [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.KeyConditionExpressions.html
4
5use core::fmt;
6
7use crate::{
8    condition::{
9        equal, greater_than, greater_than_or_equal, less_than, less_than_or_equal, Condition,
10    },
11    operand::Operand,
12    path::Path,
13    value::StringOrRef,
14};
15
16/// Represents a [DynamoDB key condition expression][1].
17///
18/// An instance can be constructed using the [`Path::key`] method, or the
19/// the `From<T: Into<Path>>` implementation.
20///
21/// See also: [`Path::key`]
22///
23/// ```
24/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
25/// use dynamodb_expression::{key::Key, Path};
26/// # use pretty_assertions::assert_eq;
27///
28/// let key: Key = "foo".parse::<Path>()?.key();
29/// let key: Key = "foo".parse::<Path>()?.into();
30/// #
31/// # Ok(())
32/// # }
33/// ```
34///
35/// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.KeyConditionExpressions.html
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
37pub struct Key {
38    // TODO: Is `Path` the right thing, here?
39    //       Probably not. Looks like it should be `Name`
40    //
41    //       > Furthermore, each primary key attribute must be defined as type string, number, or binary.
42    //
43    //       https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes
44    path: Path,
45}
46
47impl Key {
48    /// The [DynamoDB `begins_with` function][1]. True if the attribute specified by
49    ///  the [`Path`] begins with a particular substring.
50    ///
51    /// `begins_with` can take a string or a reference to an extended attribute
52    /// value. Here's an example.
53    ///
54    /// See also: [`Ref`]
55    ///
56    /// ```
57    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
58    /// use dynamodb_expression::{condition::BeginsWith, value::Ref, Path};
59    /// # use pretty_assertions::assert_eq;
60    ///
61    /// let key_condition = "foo".parse::<Path>()?.key().begins_with("T");
62    /// assert_eq!(r#"begins_with(foo, "T")"#, key_condition.to_string());
63    ///
64    /// let key_condition = "foo".parse::<Path>()?.key().begins_with(Ref::new("prefix"));
65    /// assert_eq!(r#"begins_with(foo, :prefix)"#, key_condition.to_string());
66    /// #
67    /// # Ok(())
68    /// # }
69    /// ```
70    ///
71    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Functions
72    /// [`Ref`]: crate::value::Ref
73    pub fn begins_with<T>(self, prefix: T) -> KeyCondition
74    where
75        T: Into<StringOrRef>,
76    {
77        KeyCondition {
78            condition: self.path.begins_with(prefix),
79        }
80    }
81
82    /// The [DynamoDB `BETWEEN` operator][1]. True if `self` is greater than or
83    /// equal to `lower`, and less than or equal to `upper`.
84    ///
85    /// See also: [`Path::between`]
86    ///
87    /// ```
88    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
89    /// use dynamodb_expression::{Num, Path};
90    /// # use pretty_assertions::assert_eq;
91    ///
92    /// let key_condition = "age"
93    ///     .parse::<Path>()?
94    ///     .key()
95    ///     .between(Num::new(10), Num::new(90));
96    /// assert_eq!(r#"age BETWEEN 10 AND 90"#, key_condition.to_string());
97    /// #
98    /// # Ok(())
99    /// # }
100    /// ```
101    ///
102    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Comparators
103    /// [`Path::between`]: crate::path::Path::between
104    pub fn between<L, U>(self, lower: L, upper: U) -> KeyCondition
105    where
106        L: Into<Operand>,
107        U: Into<Operand>,
108    {
109        KeyCondition {
110            condition: self.path.between(lower, upper),
111        }
112    }
113
114    /// A simple comparison that the specified attribute is equal to the
115    /// provided value.
116    pub fn equal<T>(self, right: T) -> KeyCondition
117    where
118        T: Into<Operand>,
119    {
120        KeyCondition {
121            condition: equal(self.path, right).into(),
122        }
123    }
124
125    /// A simple comparison that the specified attribute is greater than the
126    /// provided value.
127    pub fn greater_than<T>(self, right: T) -> KeyCondition
128    where
129        T: Into<Operand>,
130    {
131        KeyCondition {
132            condition: greater_than(self.path, right).into(),
133        }
134    }
135
136    /// A simple comparison that the specified attribute is greater than or
137    /// equal to the provided value.
138    pub fn greater_than_or_equal<T>(self, right: T) -> KeyCondition
139    where
140        T: Into<Operand>,
141    {
142        KeyCondition {
143            condition: greater_than_or_equal(self.path, right).into(),
144        }
145    }
146
147    /// A simple comparison that the specified attribute is less than the
148    /// provided value.
149    pub fn less_than<T>(self, right: T) -> KeyCondition
150    where
151        T: Into<Operand>,
152    {
153        KeyCondition {
154            condition: less_than(self.path, right).into(),
155        }
156    }
157
158    /// A simple comparison that the specified attribute is less than or
159    /// equal to the provided value.
160    pub fn less_than_or_equal<T>(self, right: T) -> KeyCondition
161    where
162        T: Into<Operand>,
163    {
164        KeyCondition {
165            condition: less_than_or_equal(self.path, right).into(),
166        }
167    }
168}
169
170impl<T> From<T> for Key
171where
172    T: Into<Path>,
173{
174    /// Convert something that implements `Into<Path>` into a [`Key`].
175    fn from(path: T) -> Self {
176        Self { path: path.into() }
177    }
178}
179
180/// Represents a DynamoDB [key condition expression][1]. Build an instance from
181/// the methods on [`Key`].
182///
183/// See also: [`Path::key`], [`expression::Builder::with_key_condition`]
184///
185/// ```
186/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
187/// use dynamodb_expression::{Expression, Num, Path};
188///
189/// let key_condition = "id"
190///     .parse::<Path>()?
191///     .key()
192///     .equal(Num::new(42))
193///     .and("category".parse::<Path>()?.key().begins_with("hardware."));
194///
195/// let expression = Expression::builder().with_key_condition(key_condition).build();
196/// # _ = expression;
197/// #
198/// # Ok(())
199/// # }
200/// ```
201///
202/// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.KeyConditionExpressions.html
203/// [`expression::Builder::with_key_condition`]: crate::expression::Builder::with_key_condition
204#[must_use = "Use in a DynamoDB expression with \
205    `Expression::builder().with_key_condition(key_condition)`"]
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct KeyCondition {
208    pub(crate) condition: Condition,
209}
210
211impl KeyCondition {
212    /// Combine two [`KeyCondition`]s with the `AND` operator.
213    ///
214    /// ```
215    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
216    /// use dynamodb_expression::{Num, Path};
217    /// # use pretty_assertions::assert_eq;
218    ///
219    /// let key_condition = "id"
220    ///     .parse::<Path>()?
221    ///     .key()
222    ///     .equal(Num::new(42))
223    ///     .and("category".parse::<Path>()?.key().begins_with("hardware."));
224    /// assert_eq!(r#"id = 42 AND begins_with(category, "hardware.")"#, key_condition.to_string());
225    /// #
226    /// # Ok(())
227    /// # }
228    /// ```
229    pub fn and(self, right: Self) -> Self {
230        Self {
231            condition: self.condition.and(right.condition),
232        }
233    }
234}
235
236impl fmt::Display for KeyCondition {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        self.condition.fmt(f)
239    }
240}
241
242impl From<KeyCondition> for String {
243    fn from(key_condition: KeyCondition) -> Self {
244        key_condition.condition.into()
245    }
246}
247
248#[cfg(test)]
249mod test {
250    use crate::{value::Ref, Path};
251
252    use super::Key;
253
254    #[test]
255    fn begins_with_string() {
256        // Begins with &str
257        let begins_with = Key::from("foo".parse::<Path>().unwrap()).begins_with("foo");
258        assert_eq!(r#"begins_with(foo, "foo")"#, begins_with.to_string());
259
260        // Begins with String
261        let begins_with =
262            Key::from("foo".parse::<Path>().unwrap()).begins_with(String::from("foo"));
263        assert_eq!(r#"begins_with(foo, "foo")"#, begins_with.to_string());
264
265        // Begins with &String
266        #[allow(clippy::needless_borrows_for_generic_args)]
267        let begins_with =
268            Key::from("foo".parse::<Path>().unwrap()).begins_with(&String::from("foo"));
269        assert_eq!(r#"begins_with(foo, "foo")"#, begins_with.to_string());
270
271        // Begins with &&str
272        #[allow(clippy::needless_borrows_for_generic_args)]
273        let begins_with = Key::from("foo".parse::<Path>().unwrap()).begins_with(&"foo");
274        assert_eq!(r#"begins_with(foo, "foo")"#, begins_with.to_string());
275    }
276
277    #[test]
278    fn begins_with_value_ref() {
279        let begins_with = Key::from("foo".parse::<Path>().unwrap()).begins_with(Ref::new("prefix"));
280        assert_eq!("begins_with(foo, :prefix)", begins_with.to_string());
281    }
282}