dynamodb_expression/path/element.rs
1//! DynamoDB document path elements
2
3use core::{
4 fmt::{self, Write},
5 mem,
6 str::FromStr,
7};
8
9use super::{Name, PathParseError};
10
11/// Represents a single element of a DynamoDB document [`Path`]. For example,
12/// in `foo[3][7].bar[2].baz`, the `Element`s would be `foo[3][7]`, `bar[2]`,
13/// and `baz`.
14///
15/// See also: [`Path`]
16///
17/// [`Path`]: crate::path::Path
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub enum Element {
20 Name(Name),
21 IndexedField(IndexedField),
22}
23
24impl Element {
25 /// Creates an element that represents a single attribute name in a document
26 /// path.
27 ///
28 /// See also: [`Name`], [`Element`], [`Path`]
29 ///
30 /// [`Path`]: crate::path::Path
31 pub fn new_name<T>(name: T) -> Self
32 where
33 T: Into<Name>,
34 {
35 Self::Name(name.into())
36 }
37
38 /// Creates an element that represents a single attribute name with indexed
39 /// field(s) element of a document path. For example, `foo[3]` or
40 /// `foo[7][4]`
41 ///
42 /// The `indexes` parameter, here, can be an array, slice, `Vec` of, or
43 /// single `usize`.
44 ///
45 /// ```
46 /// # use dynamodb_expression::path::Element;
47 /// # use pretty_assertions::assert_eq;
48 /// #
49 /// assert_eq!("foo[3]", Element::new_indexed_field("foo", 3).to_string());
50 /// assert_eq!("foo[3]", Element::new_indexed_field("foo", [3]).to_string());
51 /// assert_eq!("foo[3]", Element::new_indexed_field("foo", &[3]).to_string());
52 /// assert_eq!("foo[3]", Element::new_indexed_field("foo", vec![3]).to_string());
53 ///
54 /// assert_eq!("foo[7][4]", Element::new_indexed_field("foo", [7, 4]).to_string());
55 /// assert_eq!("foo[7][4]", Element::new_indexed_field("foo", &[7, 4]).to_string());
56 /// assert_eq!("foo[7][4]", Element::new_indexed_field("foo", vec![7, 4]).to_string());
57 ///
58 /// assert_eq!("foo", Element::new_indexed_field("foo", []).to_string());
59 /// assert_eq!("foo", Element::new_indexed_field("foo", &[]).to_string());
60 /// assert_eq!("foo", Element::new_indexed_field("foo", vec![]).to_string());
61 /// ```
62 ///
63 /// See also: [`IndexedField`], [`Path`], [`Path::new_indexed_field`]
64 ///
65 /// [`Path`]: crate::path::Path
66 /// [`Path::new_indexed_field`]: crate::path::Path::new_indexed_field
67 pub fn new_indexed_field<N, I>(name: N, indexes: I) -> Self
68 where
69 N: Into<Name>,
70 I: Indexes,
71 {
72 let indexes = indexes.into_indexes();
73 if indexes.is_empty() {
74 Self::new_name(name)
75 } else {
76 Self::IndexedField(IndexedField {
77 name: name.into(),
78 indexes,
79 })
80 }
81 }
82}
83
84impl fmt::Display for Element {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self {
87 Element::Name(name) => name.fmt(f),
88 Element::IndexedField(field_index) => field_index.fmt(f),
89 }
90 }
91}
92
93impl From<Element> for String {
94 fn from(element: Element) -> Self {
95 match element {
96 Element::Name(name) => name.into(),
97 Element::IndexedField(new_indexed_field) => new_indexed_field.to_string(),
98 }
99 }
100}
101
102impl From<IndexedField> for Element {
103 fn from(value: IndexedField) -> Self {
104 if value.indexes.is_empty() {
105 Self::Name(value.name)
106 } else {
107 Self::IndexedField(value)
108 }
109 }
110}
111
112impl<N, I> From<(N, I)> for Element
113where
114 N: Into<Name>,
115 I: Indexes,
116{
117 fn from((name, indexes): (N, I)) -> Self {
118 Self::new_indexed_field(name, indexes)
119 }
120}
121
122impl From<Name> for Element {
123 fn from(name: Name) -> Self {
124 Self::Name(name)
125 }
126}
127
128// Intentionally not implementing `From` string-types for `Element` to force
129// users to intentionally use a `Name` if that's what they want. Should help
130// avoid surprises when they have an indexed field, or sub-attribute.
131
132impl FromStr for Element {
133 type Err = PathParseError;
134
135 fn from_str(input: &str) -> Result<Self, Self::Err> {
136 let mut remaining = input;
137 let mut name = None;
138 let mut indexes = Vec::new();
139 while !remaining.is_empty() {
140 let open = remaining.find('[');
141 let close = remaining.find(']');
142
143 match (open, close) {
144 (None, None) => {
145 if name.is_some() {
146 // `bar` in `foo[0]bar`
147 return Err(PathParseError);
148 }
149
150 // No more braces. Consume the rest of the string.
151 name = Some(mem::take(&mut remaining));
152 break;
153 }
154 (None, Some(_close)) => return Err(PathParseError),
155 (Some(_open), None) => return Err(PathParseError),
156 (Some(open), Some(close)) => {
157 if open >= close {
158 // `foo][`
159 return Err(PathParseError);
160 }
161
162 if name.is_none() {
163 if open > 0 {
164 name = Some(&remaining[..open]);
165 } else {
166 // The string starts with a '['. E.g.:
167 // `[]foo`
168 return Err(PathParseError);
169 }
170 } else if open > 0 {
171 // We've already got the name but we just found another after a closing bracket.
172 // E.g, `bar[0]` in `foo[7]bar[0]`
173 return Err(PathParseError);
174 }
175
176 // The value between the braces should be a usize.
177 let index: usize = remaining[open + 1..close]
178 .parse()
179 .map_err(|_| PathParseError)?;
180 indexes.push(index);
181
182 remaining = &remaining[close + 1..];
183 }
184 }
185 }
186
187 Ok(if indexes.is_empty() {
188 Self::Name(input.into())
189 } else {
190 if !remaining.is_empty() {
191 // Shouldn't be able to get there.
192 // If we do, something above changed and there's a bug.
193 return Err(PathParseError);
194 }
195
196 let name = name.ok_or(PathParseError)?;
197
198 indexes.shrink_to_fit();
199
200 Self::IndexedField(IndexedField {
201 name: name.into(),
202 indexes,
203 })
204 })
205 }
206}
207
208/// Represents a type of [`Element`] of a DynamoDB document [`Path`] that is a
209/// [`Name`] with one or more indexes. For example, in `foo[3][7].bar[2].baz`,
210/// the elements `foo[3][7]` and `bar[2]` would both be represented as an
211/// `IndexedField`.
212///
213/// Created via `Element::from`, [`Element::new_indexed_field`], and
214/// [`Path::new_indexed_field`].
215///
216/// [`Path::new_indexed_field`]: crate::path::Path::new_indexed_field
217/// [`Path`]: crate::path::Path
218#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
219pub struct IndexedField {
220 pub(crate) name: Name,
221 indexes: Vec<usize>,
222}
223
224impl fmt::Display for IndexedField {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 self.name.fmt(f)?;
227 self.indexes.iter().try_for_each(|index| {
228 f.write_char('[')?;
229 index.fmt(f)?;
230 f.write_char(']')
231 })
232 }
233}
234
235/// Used for [`IndexedField`]. An array, slice, `Vec` of, or single `usize`.
236///
237/// See also: [`Element::new_indexed_field`], [`Path::new_indexed_field`]
238///
239/// [`Path::new_indexed_field`]: crate::path::Path::new_indexed_field
240pub trait Indexes {
241 fn into_indexes(self) -> Vec<usize>;
242}
243
244impl Indexes for usize {
245 fn into_indexes(self) -> Vec<usize> {
246 vec![self]
247 }
248}
249
250impl Indexes for Vec<usize> {
251 fn into_indexes(self) -> Vec<usize> {
252 self
253 }
254}
255
256impl Indexes for &[usize] {
257 fn into_indexes(self) -> Vec<usize> {
258 self.to_vec()
259 }
260}
261
262impl<const N: usize> Indexes for [usize; N] {
263 fn into_indexes(self) -> Vec<usize> {
264 self.to_vec()
265 }
266}
267
268impl<const N: usize> Indexes for &[usize; N] {
269 fn into_indexes(self) -> Vec<usize> {
270 self.to_vec()
271 }
272}
273
274#[cfg(test)]
275mod test {
276 use pretty_assertions::assert_eq;
277
278 use crate::{Num, Path};
279
280 use super::{Element, Name};
281
282 #[test]
283 fn display_name() {
284 let path = Element::new_name("foo");
285 assert_eq!("foo", path.to_string());
286 }
287
288 #[test]
289 fn display_indexed() {
290 // Also tests that `Element::new_indexed_field()` can accept a few different types of input.
291
292 // From a usize
293 let path = Element::new_indexed_field("foo", 42);
294 assert_eq!("foo[42]", path.to_string());
295
296 // From an array of usize
297 let path = Element::new_indexed_field("foo", [42]);
298 assert_eq!("foo[42]", path.to_string());
299
300 // From a slice of usize
301 let path = Element::new_indexed_field("foo", &([42, 37, 9])[..]);
302 assert_eq!("foo[42][37][9]", path.to_string());
303 }
304
305 #[test]
306 fn display_path() {
307 let path: Path = ["foo", "bar"].into_iter().map(Name::from).collect();
308 assert_eq!("foo.bar", path.to_string());
309
310 let path = Path::from_iter([
311 Element::new_name("foo"),
312 Element::new_indexed_field("bar", 42),
313 ]);
314 assert_eq!("foo.bar[42]", path.to_string());
315
316 // TODO: I'm not sure this is a legal path based on these examples:
317 // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.Attributes.html#Expressions.Attributes.NestedElements.DocumentPathExamples
318 // Test whether it's valid and remove this comment or handle it appropriately.
319 let path = Path::from_iter([
320 Element::new_indexed_field("foo", 42),
321 Element::new_name("bar"),
322 ]);
323 assert_eq!("foo[42].bar", path.to_string());
324 }
325
326 #[test]
327 fn size() {
328 assert_eq!(
329 "size(a) = 0",
330 "a".parse::<Path>()
331 .unwrap()
332 .size()
333 .equal(Num::new(0))
334 .to_string()
335 );
336 }
337}