1use crate::Slug;
2use std::collections::HashMap;
3
4#[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 pub fn empty() -> Self {
15 Self {
16 map: HashMap::new(),
17 keys: Vec::new(),
18 }
19 }
20
21 pub fn new(input: HashMap<Slug, V>) -> Self {
24 let mut keys = input.keys().cloned().collect::<Vec<_>>();
26
27 keys.sort();
29
30 Self { map: input, keys }
31 }
32
33 pub fn from(input: HashMap<String, V>) -> Self {
39 let mut map: HashMap<Slug, V> = HashMap::with_capacity(input.len());
41
42 for (k, v) in input {
44 map.insert(Slug::new(k), v);
45 }
46
47 Self::new(map)
48 }
49
50 pub fn zip<F>(input: HashMap<String, V>, mut zipper: F) -> Self
56 where
57 F: FnMut(V, V) -> V,
58 {
59 let mut map: HashMap<Slug, V> = HashMap::with_capacity(input.len());
61
62 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 pub fn contains_key(&self, key: impl AsRef<str>) -> bool {
82 self.find_key(key).is_some()
83 }
84
85 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 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 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 let map = HashMap::from([
164 (Slug::new("HelloWorld"), 42),
165 (Slug::new("test123"), 7),
166 (Slug::new("++abc"), 999),
167 ]);
168
169 let slug_map = SlugMap::new(map);
171
172 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 let map = HashMap::from([
200 ("HelloWorld".to_string(), 1),
201 ("HELLO_WORLD".to_string(), 2),
202 ("test123".to_string(), 3),
203 ]);
204
205 let slug_map = SlugMap::from(map);
207
208 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 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 let slug_map = SlugMap::zip(map, |a, b| a + b);
232
233 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 let map = HashMap::from([
243 ("A".to_string(), 1),
244 ("a".to_string(), 2),
245 ("A!".to_string(), 3),
246 ]);
247
248 let slug_map = SlugMap::zip(map, |a, b| a * b);
250
251 assert_eq!(slug_map.get("--a"), Some(&6));
253 }
254
255 #[test]
256 fn test_empty_map() {
257 let map: HashMap<Slug, i32> = HashMap::new();
259
260 let slug_map = SlugMap::new(map);
262
263 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 let map = HashMap::from([("!!!".to_string(), 99), ("***".to_string(), 42)]);
272
273 let slug_map = SlugMap::from(map);
275
276 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 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 let mapped = slug_map.map(|key, value| format!("{}-{}", key, value));
299
300 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}