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}