1use std::collections::HashMap;
10use std::hash::{Hash, Hasher};
11
12#[derive(Debug, Clone)]
30pub struct CaseInsensitiveStr {
31 normalized: String, }
33
34impl CaseInsensitiveStr {
35 pub fn new(s: impl Into<String>) -> Self {
39 Self {
40 normalized: s.into().to_lowercase(),
41 }
42 }
43
44 pub fn as_str(&self) -> &str {
46 &self.normalized
47 }
48}
49
50impl PartialEq for CaseInsensitiveStr {
51 fn eq(&self, other: &Self) -> bool {
52 self.normalized == other.normalized
53 }
54}
55
56impl Eq for CaseInsensitiveStr {}
57
58impl Hash for CaseInsensitiveStr {
59 fn hash<H: Hasher>(&self, state: &mut H) {
60 self.normalized.hash(state);
61 }
62}
63
64impl From<&str> for CaseInsensitiveStr {
65 fn from(s: &str) -> Self {
66 Self::new(s)
67 }
68}
69
70impl From<String> for CaseInsensitiveStr {
71 fn from(s: String) -> Self {
72 Self::new(s)
73 }
74}
75
76impl AsRef<str> for CaseInsensitiveStr {
77 fn as_ref(&self) -> &str {
78 &self.normalized
79 }
80}
81
82pub type CaseInsensitiveMap<V> = HashMap<CaseInsensitiveStr, V>;
87
88#[inline]
106pub fn qualify_column(alias: &str, property: &str) -> String {
107 format!("{}__{}", alias.to_lowercase(), property.to_lowercase())
108}
109
110pub trait CaseInsensitiveLookup<V> {
144 fn get_ci(&self, key: &str) -> Option<&V>;
148
149 fn contains_key_ci(&self, key: &str) -> bool;
151
152 fn get_mut_ci(&mut self, key: &str) -> Option<&mut V>;
154}
155
156impl<V> CaseInsensitiveLookup<V> for HashMap<String, V> {
157 fn get_ci(&self, key: &str) -> Option<&V> {
158 if let Some(v) = self.get(key) {
160 return Some(v);
161 }
162 let key_lower = key.to_lowercase();
164 self.iter()
165 .find(|(k, _)| k.to_lowercase() == key_lower)
166 .map(|(_, v)| v)
167 }
168
169 fn contains_key_ci(&self, key: &str) -> bool {
170 self.get_ci(key).is_some()
171 }
172
173 fn get_mut_ci(&mut self, key: &str) -> Option<&mut V> {
174 let key_lower = key.to_lowercase();
176 let actual_key = self.keys().find(|k| k.to_lowercase() == key_lower).cloned();
177
178 actual_key.and_then(|k| self.get_mut(&k))
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_case_insensitive_str_equality() {
189 let a = CaseInsensitiveStr::new("Person");
190 let b = CaseInsensitiveStr::new("person");
191 let c = CaseInsensitiveStr::new("PERSON");
192 let d = CaseInsensitiveStr::new("PeRsOn");
193
194 assert_eq!(a, b);
195 assert_eq!(b, c);
196 assert_eq!(a, c);
197 assert_eq!(c, d);
198 }
199
200 #[test]
201 fn test_case_insensitive_str_inequality() {
202 let a = CaseInsensitiveStr::new("Person");
203 let b = CaseInsensitiveStr::new("Company");
204
205 assert_ne!(a, b);
206 }
207
208 #[test]
209 fn test_case_insensitive_str_hash() {
210 use std::collections::HashSet;
211
212 let mut set = HashSet::new();
213 set.insert(CaseInsensitiveStr::new("Person"));
214
215 assert!(set.contains(&CaseInsensitiveStr::new("person")));
217 assert!(set.contains(&CaseInsensitiveStr::new("PERSON")));
218 assert!(set.contains(&CaseInsensitiveStr::new("Person")));
219
220 assert!(!set.contains(&CaseInsensitiveStr::new("Company")));
222 }
223
224 #[test]
225 fn test_case_insensitive_map() {
226 let mut map: CaseInsensitiveMap<i32> = HashMap::new();
227 map.insert(CaseInsensitiveStr::new("Person"), 1);
228 map.insert(CaseInsensitiveStr::new("Company"), 2);
229
230 assert_eq!(map.get(&CaseInsensitiveStr::new("person")), Some(&1));
232 assert_eq!(map.get(&CaseInsensitiveStr::new("PERSON")), Some(&1));
233 assert_eq!(map.get(&CaseInsensitiveStr::new("Person")), Some(&1));
234 assert_eq!(map.get(&CaseInsensitiveStr::new("PeRsOn")), Some(&1));
235
236 assert_eq!(map.get(&CaseInsensitiveStr::new("company")), Some(&2));
237 assert_eq!(map.get(&CaseInsensitiveStr::new("COMPANY")), Some(&2));
238
239 assert_eq!(map.get(&CaseInsensitiveStr::new("Unknown")), None);
240 }
241
242 #[test]
243 fn test_case_insensitive_lookup_trait() {
244 let mut map = HashMap::new();
245 map.insert("Person".to_string(), 1);
246 map.insert("Company".to_string(), 2);
247 map.insert("fullName".to_string(), 3);
248
249 assert_eq!(map.get_ci("person"), Some(&1));
251 assert_eq!(map.get_ci("PERSON"), Some(&1));
252 assert_eq!(map.get_ci("Person"), Some(&1));
253 assert_eq!(map.get_ci("PeRsOn"), Some(&1));
254
255 assert_eq!(map.get_ci("company"), Some(&2));
256 assert_eq!(map.get_ci("COMPANY"), Some(&2));
257
258 assert_eq!(map.get_ci("fullname"), Some(&3));
259 assert_eq!(map.get_ci("FULLNAME"), Some(&3));
260 assert_eq!(map.get_ci("FullName"), Some(&3));
261
262 assert_eq!(map.get_ci("Unknown"), None);
263
264 assert!(map.contains_key_ci("person"));
266 assert!(map.contains_key_ci("COMPANY"));
267 assert!(map.contains_key_ci("FullName"));
268 assert!(!map.contains_key_ci("Unknown"));
269 }
270
271 #[test]
272 fn test_case_insensitive_lookup_exact_match_fast_path() {
273 let mut map = HashMap::new();
274 map.insert("Person".to_string(), 1);
275
276 assert_eq!(map.get_ci("Person"), Some(&1));
278
279 assert_eq!(map.get_ci("person"), Some(&1));
281 assert_eq!(map.get_ci("PERSON"), Some(&1));
282 }
283
284 #[test]
285 fn test_case_insensitive_str_as_str() {
286 let s = CaseInsensitiveStr::new("Person");
287 assert_eq!(s.as_str(), "person"); }
289
290 #[test]
291 fn test_case_insensitive_str_from_string() {
292 let s = String::from("Person");
293 let ci_str: CaseInsensitiveStr = s.into();
294 assert_eq!(ci_str.as_str(), "person");
295 }
296
297 #[test]
298 fn test_case_insensitive_str_from_str() {
299 let ci_str: CaseInsensitiveStr = "Person".into();
300 assert_eq!(ci_str.as_str(), "person");
301 }
302
303 #[test]
304 fn test_case_insensitive_map_insertion_deduplication() {
305 let mut map: CaseInsensitiveMap<i32> = HashMap::new();
306
307 map.insert(CaseInsensitiveStr::new("Person"), 1);
309 map.insert(CaseInsensitiveStr::new("person"), 2);
310 map.insert(CaseInsensitiveStr::new("PERSON"), 3);
311
312 assert_eq!(map.len(), 1);
314 assert_eq!(map.get(&CaseInsensitiveStr::new("person")), Some(&3));
315 }
316
317 #[test]
318 fn test_get_mut_ci() {
319 let mut map = HashMap::new();
320 map.insert("Person".to_string(), 1);
321 map.insert("Company".to_string(), 2);
322
323 if let Some(v) = map.get_mut_ci("person") {
325 *v = 10;
326 }
327 assert_eq!(map.get_ci("Person"), Some(&10));
328
329 if let Some(v) = map.get_mut_ci("COMPANY") {
330 *v = 20;
331 }
332 assert_eq!(map.get_ci("company"), Some(&20));
333
334 assert!(map.get_mut_ci("Unknown").is_none());
336 }
337
338 #[test]
339 fn test_property_name_normalization() {
340 let mut map = HashMap::new();
342 map.insert("fullName".to_string(), 1);
343 map.insert("isActive".to_string(), 2);
344 map.insert("numFollowers".to_string(), 3);
345
346 assert_eq!(map.get_ci("fullname"), Some(&1));
348 assert_eq!(map.get_ci("FULLNAME"), Some(&1));
349 assert_eq!(map.get_ci("FullName"), Some(&1));
350
351 assert_eq!(map.get_ci("isactive"), Some(&2));
352 assert_eq!(map.get_ci("ISACTIVE"), Some(&2));
353 assert_eq!(map.get_ci("IsActive"), Some(&2));
354
355 assert_eq!(map.get_ci("numfollowers"), Some(&3));
356 assert_eq!(map.get_ci("NUMFOLLOWERS"), Some(&3));
357 assert_eq!(map.get_ci("NumFollowers"), Some(&3));
358 }
359
360 #[test]
361 fn test_qualify_column() {
362 use super::qualify_column;
363
364 assert_eq!(qualify_column("p", "name"), "p__name");
366 assert_eq!(qualify_column("person", "age"), "person__age");
367
368 assert_eq!(qualify_column("P", "Name"), "p__name");
370 assert_eq!(qualify_column("PERSON", "AGE"), "person__age");
371 assert_eq!(qualify_column("Person", "fullName"), "person__fullname");
372
373 assert_eq!(qualify_column("MyVar", "IsActive"), "myvar__isactive");
375 assert_eq!(qualify_column("a", "NumFollowers"), "a__numfollowers");
376 }
377}