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}