Skip to main content

dynamodb_expression/value/set/
mod.rs

1mod binary_set;
2mod num_set;
3mod string_set;
4
5pub use binary_set::BinarySet;
6pub use num_set::NumSet;
7pub use string_set::StringSet;
8
9use core::fmt;
10
11use aws_sdk_dynamodb::types::AttributeValue;
12
13use super::base64;
14
15/// A collection of DynamoDB values that are all the same type and unique.
16///
17/// <https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes>
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub enum Set {
20    StringSet(StringSet),
21    NumSet(NumSet),
22    BinarySet(BinarySet),
23}
24
25impl Set {
26    /// Creates a value to use as a [DynamoDB string set][1].
27    ///
28    /// See also: [`StringSet::new`], [`Value::new_string_set`]
29    ///
30    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes
31    /// [`Value::new_string_set`]: crate::Value::new_string_set
32    pub fn new_string_set<T>(string_set: T) -> Self
33    where
34        T: Into<StringSet>,
35    {
36        string_set.into().into()
37    }
38
39    /// Creates a value to use as a [DynamoDB number set][1].
40    ///
41    /// See also: [`NumSet::new`], [`Value::new_num_set`]
42    ///
43    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes
44    /// [`Value::new_num_set`]: crate::Value::new_num_set
45    pub fn new_num_set<T>(num_set: T) -> Self
46    where
47        T: Into<NumSet>,
48    {
49        num_set.into().into()
50    }
51
52    /// Creates a value to use as a [DynamoDB binary set][1].
53    ///
54    /// See also: [`BinarySet::new`], [`Value::new_binary_set`]
55    ///
56    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes
57    /// [`Value::new_binary_set`]: crate::Value::new_binary_set
58    pub fn new_binary_set<T>(binary_set: T) -> Self
59    where
60        T: Into<BinarySet>,
61    {
62        binary_set.into().into()
63    }
64
65    // Intentionally not using `impl From<SetValue> for AttributeValue` because
66    // I don't want to make this a public API people rely on. The purpose of this
67    // crate is not to make creating `AttributeValues` easier. They should try
68    // `serde_dynamo`.
69    pub(super) fn into_attribute_value(self) -> AttributeValue {
70        match self {
71            Set::StringSet(set) => set.into_attribute_value(),
72            Set::NumSet(set) => set.into_attribute_value(),
73            Set::BinarySet(set) => set.into_attribute_value(),
74        }
75    }
76}
77
78impl fmt::Display for Set {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            Set::StringSet(set) => set.fmt(f),
82            Set::NumSet(set) => set.fmt(f),
83            Set::BinarySet(set) => set.fmt(f),
84        }
85    }
86}
87
88impl From<StringSet> for Set {
89    fn from(string_set: StringSet) -> Self {
90        Self::StringSet(string_set)
91    }
92}
93
94impl From<NumSet> for Set {
95    fn from(num_set: NumSet) -> Self {
96        Self::NumSet(num_set)
97    }
98}
99
100impl From<BinarySet> for Set {
101    fn from(binary_set: BinarySet) -> Self {
102        Self::BinarySet(binary_set)
103    }
104}
105
106#[cfg(test)]
107mod test {
108    use std::{cell::RefCell, iter::FusedIterator};
109
110    use itertools::Itertools;
111    use pretty_assertions::assert_eq;
112
113    use crate::{
114        value::{base64, Set},
115        Num,
116    };
117
118    #[test]
119    fn string_set_display() {
120        let set = Set::new_string_set(["foo", "bar", "!@#$%^&*()-=_+\"'{}[]\\|;:<>,./?`~"]);
121        assert_eq!(
122            r#"["!@#$%^&*()-=_+\"'{}[]\\|;:<>,./?`~", "bar", "foo"]"#,
123            set.to_string()
124        );
125
126        let deserialized: Vec<String> =
127            serde_json::from_str(&set.to_string()).expect("Must be valid JSON");
128        assert_eq!(
129            vec!["!@#$%^&*()-=_+\"'{}[]\\|;:<>,./?`~", "bar", "foo"],
130            deserialized
131        );
132    }
133
134    #[test]
135    fn num_set_display() {
136        let set = Set::new_num_set([-1, 0, 1, 42]);
137        assert_eq!("[-1, 0, 1, 42]", set.to_string());
138
139        let deserialized: Vec<i32> =
140            serde_json::from_str(&set.to_string()).expect("Must be valid JSON");
141        assert_eq!(vec![-1, 0, 1, 42], deserialized);
142
143        let set = Set::new_num_set([
144            Num::new_lower_exp(f32::MIN),
145            Num::new(0.0),
146            Num::new(1000),
147            Num::new_upper_exp(f32::MAX),
148            Num::new(9.2),
149        ]);
150        assert_eq!(
151            "[\
152                -3.4028235e38, \
153                0, \
154                1000, \
155                3.4028235E38, \
156                9.2\
157            ]",
158            set.to_string()
159        );
160
161        let deserialized: Vec<f32> =
162            serde_json::from_str(&set.to_string()).expect("Must be valid JSON");
163        assert_eq!(vec![f32::MIN, 0.0, 1000.0, f32::MAX, 9.2], deserialized);
164    }
165
166    #[test]
167    fn binary_set_display() {
168        // These strings chosen because they produce base64 strings with all the
169        // non-alphanumeric chars in the base64 set ('+', '/', and the padding
170        // char, '='). Used `find_tricky_base64()`, below.
171        let set = Set::new_binary_set(["  > ", "  ? "]);
172        assert_eq!(r#"["ICA+IA==", "ICA/IA=="]"#, set.to_string());
173
174        let deserialized: Vec<String> =
175            serde_json::from_str(&set.to_string()).expect("Must be valid JSON");
176        assert_eq!(vec!["ICA+IA==", "ICA/IA=="], deserialized);
177    }
178
179    #[test]
180    #[ignore = "Just used to find more base64 for JSON encoding testing"]
181    fn find_tricky_base64() {
182        /// Visible ASCII characters
183        fn charset(
184        ) -> impl ExactSizeIterator<Item = char> + DoubleEndedIterator + FusedIterator + Clone
185        {
186            (32..127).map(char::from_u32).map(Option::unwrap)
187        }
188
189        // Check that the encoded value contains at least one of the
190        // non-alphanumeric (and non-padding) base64 chars.
191        let specials = RefCell::new(['+', '/'].into_iter().peekable());
192        [charset(), charset(), charset(), charset()]
193            .into_iter()
194            .multi_cartesian_product()
195            .take_while(|_| specials.borrow_mut().peek().is_some())
196            .map(String::from_iter)
197            .enumerate() // Just to see how many iterations this takes
198            .map(|(i, raw)| {
199                let encoded = base64(&raw);
200                (i, raw, encoded)
201            })
202            .filter(|(_i, _raw, encoded)| {
203                if encoded.contains(specials.borrow_mut().peek().cloned().unwrap()) {
204                    specials.borrow_mut().next();
205                    true
206                } else {
207                    false
208                }
209            })
210            .for_each(|(index, raw, encoded)| {
211                println!(
212                    "The encoded version of iteration {index}, {raw:?}, \
213                        includes special characters: {encoded}"
214                )
215            });
216    }
217}