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