strut_deserialize/slug/
map.rs

1use crate::Slug;
2use std::collections::HashMap;
3
4/// An immutable [`HashMap`] that uses [`Slug`]s as keys and allows efficiently
5/// searching for matching entries by string reference.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct SlugMap<V> {
8    map: HashMap<Slug, V>,
9    keys: Vec<Slug>,
10}
11
12impl<V> SlugMap<V> {
13    /// Creates an empty [`SlugMap`].
14    pub fn empty() -> Self {
15        Self {
16            map: HashMap::new(),
17            keys: Vec::new(),
18        }
19    }
20
21    /// Consumes and transforms the given [`HashMap`] with [`Slug`] keys into
22    /// a [`SlugMap`].
23    pub fn new(input: HashMap<Slug, V>) -> Self {
24        // Collect the keys
25        let mut keys = input.keys().cloned().collect::<Vec<_>>();
26
27        // IMPORTANT: store the keys sorted
28        keys.sort();
29
30        Self { map: input, keys }
31    }
32
33    /// Consumes and transforms the given [`HashMap`] with [`String`] keys into
34    /// a [`SlugMap`].
35    ///
36    /// If any two keys in the input resolve to the same [`Slug`] — only one of
37    /// two values is retained.
38    pub fn from(input: HashMap<String, V>) -> Self {
39        // Prepare storage
40        let mut map: HashMap<Slug, V> = HashMap::with_capacity(input.len());
41
42        // Convert string keys to slug keys, consuming the input map
43        for (k, v) in input {
44            map.insert(Slug::new(k), v);
45        }
46
47        Self::new(map)
48    }
49
50    /// Consumes and transforms the given [`HashMap`] with [`String`] keys into
51    /// a [`SlugMap`].
52    ///
53    /// If any two keys in the input resolve to the same [`Slug`] — the two
54    /// values are consumed and merged using the provided `zipper` function.
55    pub fn zip<F>(input: HashMap<String, V>, mut zipper: F) -> Self
56    where
57        F: FnMut(V, V) -> V,
58    {
59        // Prepare storage
60        let mut map: HashMap<Slug, V> = HashMap::with_capacity(input.len());
61
62        // Convert string keys to slug keys, consuming the input map
63        for (k, v) in input {
64            let slug = Slug::new(k);
65
66            if let Some(existing) = map.remove(&slug) {
67                let merged = zipper(existing, v);
68                map.insert(slug, merged);
69            } else {
70                map.insert(slug, v);
71            }
72        }
73
74        Self::new(map)
75    }
76}
77
78impl<V> SlugMap<V> {
79    /// Returns `true` if the map contains a value for the specified key,
80    /// comparing the keys as [`Slug`]s.
81    pub fn contains_key(&self, key: impl AsRef<str>) -> bool {
82        self.find_key(key).is_some()
83    }
84
85    /// Returns a reference to the value corresponding to the key, comparing the
86    /// keys as [`Slug`]s.
87    ///
88    /// If the map contains a value for the [`Slug`] key `"somekey"`, then
89    /// calling this method with anything that compares equally to that slug
90    /// (e.g., `"SomeKey"` or `"__some_key"`) will find and return that value.
91    pub fn get(&self, key: impl AsRef<str>) -> Option<&V> {
92        self.find_key(key)
93            .and_then(|found_key| self.map.get(found_key))
94    }
95
96    /// Returns a [`Slug`] contained in this map and matching the given `key`,
97    /// if one exists.
98    fn find_key(&self, key: impl AsRef<str>) -> Option<&Slug> {
99        let input = key.as_ref();
100
101        self.keys
102            .binary_search_by(|current_key| Slug::cmp_as_slugs(current_key, input))
103            .ok()
104            .map(|found_index| &self.keys[found_index])
105    }
106
107    /// Maps the values of this [`SlugMap`] using the given `mapper` function.
108    /// Leaves the keys unchanged.
109    pub fn map<NV, F>(self, mut mapper: F) -> SlugMap<NV>
110    where
111        F: FnMut(&Slug, V) -> NV,
112    {
113        let mut map = HashMap::with_capacity(self.map.len());
114
115        for (k, v) in self.map {
116            let new_value = mapper(&k, v);
117            map.insert(k, new_value);
118        }
119
120        SlugMap {
121            map,
122            keys: self.keys,
123        }
124    }
125}
126
127const _: () = {
128    impl<V> Default for SlugMap<V> {
129        fn default() -> Self {
130            Self::empty()
131        }
132    }
133
134    impl<S, V> FromIterator<(S, V)> for SlugMap<V>
135    where
136        S: Into<Slug>,
137    {
138        fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
139            let handles = iter.into_iter().map(|(k, v)| (k.into(), v)).collect();
140
141            Self::new(handles)
142        }
143    }
144
145    impl<const N: usize, S, V> From<[(S, V); N]> for SlugMap<V>
146    where
147        S: Into<Slug>,
148    {
149        fn from(value: [(S, V); N]) -> Self {
150            value.into_iter().collect()
151        }
152    }
153};
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use std::collections::HashMap;
159
160    #[test]
161    fn new_get() {
162        // Given
163        let map = HashMap::from([
164            (Slug::new("HelloWorld"), 42),
165            (Slug::new("test123"), 7),
166            (Slug::new("++abc"), 999),
167        ]);
168
169        // When
170        let slug_map = SlugMap::new(map);
171
172        // Then
173        assert!(slug_map.contains_key("abc"));
174        assert!(slug_map.contains_key("HELLO_WORLD"));
175        assert!(slug_map.contains_key("hello-world"));
176        assert!(slug_map.contains_key("test_123"));
177        assert!(slug_map.contains_key("TEST123"));
178        assert!(!slug_map.contains_key("notfound"));
179
180        assert_eq!(
181            slug_map.keys,
182            vec![
183                Slug::new("abc"),
184                Slug::new("helloworld"),
185                Slug::new("test123")
186            ],
187        );
188        assert_eq!(slug_map.get("abc"), Some(&999));
189        assert_eq!(slug_map.get("HELLO_WORLD"), Some(&42));
190        assert_eq!(slug_map.get("hello-world"), Some(&42));
191        assert_eq!(slug_map.get("test_123"), Some(&7));
192        assert_eq!(slug_map.get("TEST123"), Some(&7));
193        assert_eq!(slug_map.get("notfound"), None);
194    }
195
196    #[test]
197    fn from() {
198        // Given
199        let map = HashMap::from([
200            ("HelloWorld".to_string(), 1),
201            ("HELLO_WORLD".to_string(), 2),
202            ("test123".to_string(), 3),
203        ]);
204
205        // When
206        let slug_map = SlugMap::from(map);
207
208        // Then
209        assert!(matches!(slug_map.get("HelloWorld"), Some(_)));
210        assert!(matches!(slug_map.get("HELLO_WORLD"), Some(_)));
211        assert!(matches!(slug_map.get("!HelloWorld+"), Some(_)));
212        assert!(matches!(slug_map.get("hellO()World"), Some(_)));
213        assert_eq!(slug_map.get("HelloWorld"), slug_map.get("HELLO_WORLD"));
214        assert_eq!(slug_map.get("!HelloWorld+"), slug_map.get("hellO()World"));
215        assert_eq!(slug_map.get("test123"), Some(&3));
216        assert_eq!(slug_map.get("TEST123"), Some(&3));
217        assert_eq!(slug_map.get("notfound"), None);
218    }
219
220    #[test]
221    fn zip() {
222        // Given
223        let map = HashMap::from([
224            ("HelloWorld".to_string(), 1),
225            ("HELLO_WORLD".to_string(), 2),
226            ("test123".to_string(), 3),
227            ("TEST123".to_string(), 5),
228        ]);
229
230        // When
231        let slug_map = SlugMap::zip(map, |a, b| a + b);
232
233        // Then
234        assert_eq!(slug_map.get("hello_world"), Some(&3));
235        assert_eq!(slug_map.get("test123"), Some(&8));
236        assert_eq!(slug_map.get("notfound"), None);
237    }
238
239    #[test]
240    fn zip_multiple() {
241        // Given
242        let map = HashMap::from([
243            ("A".to_string(), 1),
244            ("a".to_string(), 2),
245            ("A!".to_string(), 3),
246        ]);
247
248        // When
249        let slug_map = SlugMap::zip(map, |a, b| a * b);
250
251        // Then
252        assert_eq!(slug_map.get("--a"), Some(&6));
253    }
254
255    #[test]
256    fn test_empty_map() {
257        // Given
258        let map: HashMap<Slug, i32> = HashMap::new();
259
260        // When
261        let slug_map = SlugMap::new(map);
262
263        // Then
264        assert_eq!(slug_map.keys.len(), 0);
265        assert_eq!(slug_map.get("anything"), None);
266    }
267
268    #[test]
269    fn only_non_alphanumeric() {
270        // Given
271        let map = HashMap::from([("!!!".to_string(), 99), ("***".to_string(), 42)]);
272
273        // When
274        let slug_map = SlugMap::from(map);
275
276        // Then
277        assert!(matches!(slug_map.get(""), Some(_)));
278        assert!(matches!(slug_map.get("!!!"), Some(_)));
279        assert!(matches!(slug_map.get("***"), Some(_)));
280        assert_eq!(slug_map.get(""), slug_map.get("!!!"));
281        assert_eq!(slug_map.get("***"), slug_map.get("!!!"));
282    }
283
284    #[test]
285    fn map() {
286        // Given
287        let map = HashMap::from([
288            (Slug::new("HelloWorld"), 42),
289            (Slug::new("test123"), -7),
290            (Slug::new("++abc"), 999),
291        ]);
292        let slug_map = SlugMap::new(map);
293        fn get<'a>(map: &'a SlugMap<String>, key: &str) -> Option<&'a str> {
294            map.get(key).map(|v| v.as_str())
295        }
296
297        // When
298        let mapped = slug_map.map(|key, value| format!("{}-{}", key, value));
299
300        // Then
301        assert_eq!(get(&mapped, "hello_world"), Some("helloworld-42"),);
302        assert_eq!(get(&mapped, "test123"), Some("test123--7"));
303        assert_eq!(get(&mapped, "TEST123"), Some("test123--7"));
304        assert_eq!(get(&mapped, "++abc"), Some("abc-999"));
305        assert_eq!(get(&mapped, "abc"), Some("abc-999"));
306    }
307}