use std::collections::HashMap;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone)]
pub struct CaseInsensitiveStr {
normalized: String, }
impl CaseInsensitiveStr {
pub fn new(s: impl Into<String>) -> Self {
Self {
normalized: s.into().to_lowercase(),
}
}
pub fn as_str(&self) -> &str {
&self.normalized
}
}
impl PartialEq for CaseInsensitiveStr {
fn eq(&self, other: &Self) -> bool {
self.normalized == other.normalized
}
}
impl Eq for CaseInsensitiveStr {}
impl Hash for CaseInsensitiveStr {
fn hash<H: Hasher>(&self, state: &mut H) {
self.normalized.hash(state);
}
}
impl From<&str> for CaseInsensitiveStr {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for CaseInsensitiveStr {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl AsRef<str> for CaseInsensitiveStr {
fn as_ref(&self) -> &str {
&self.normalized
}
}
pub type CaseInsensitiveMap<V> = HashMap<CaseInsensitiveStr, V>;
#[inline]
pub fn qualify_column(alias: &str, property: &str) -> String {
format!("{}__{}", alias.to_lowercase(), property.to_lowercase())
}
pub trait CaseInsensitiveLookup<V> {
fn get_ci(&self, key: &str) -> Option<&V>;
fn contains_key_ci(&self, key: &str) -> bool;
fn get_mut_ci(&mut self, key: &str) -> Option<&mut V>;
}
impl<V> CaseInsensitiveLookup<V> for HashMap<String, V> {
fn get_ci(&self, key: &str) -> Option<&V> {
if let Some(v) = self.get(key) {
return Some(v);
}
let key_lower = key.to_lowercase();
self.iter()
.find(|(k, _)| k.to_lowercase() == key_lower)
.map(|(_, v)| v)
}
fn contains_key_ci(&self, key: &str) -> bool {
self.get_ci(key).is_some()
}
fn get_mut_ci(&mut self, key: &str) -> Option<&mut V> {
let key_lower = key.to_lowercase();
let actual_key = self.keys().find(|k| k.to_lowercase() == key_lower).cloned();
actual_key.and_then(|k| self.get_mut(&k))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_case_insensitive_str_equality() {
let a = CaseInsensitiveStr::new("Person");
let b = CaseInsensitiveStr::new("person");
let c = CaseInsensitiveStr::new("PERSON");
let d = CaseInsensitiveStr::new("PeRsOn");
assert_eq!(a, b);
assert_eq!(b, c);
assert_eq!(a, c);
assert_eq!(c, d);
}
#[test]
fn test_case_insensitive_str_inequality() {
let a = CaseInsensitiveStr::new("Person");
let b = CaseInsensitiveStr::new("Company");
assert_ne!(a, b);
}
#[test]
fn test_case_insensitive_str_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(CaseInsensitiveStr::new("Person"));
assert!(set.contains(&CaseInsensitiveStr::new("person")));
assert!(set.contains(&CaseInsensitiveStr::new("PERSON")));
assert!(set.contains(&CaseInsensitiveStr::new("Person")));
assert!(!set.contains(&CaseInsensitiveStr::new("Company")));
}
#[test]
fn test_case_insensitive_map() {
let mut map: CaseInsensitiveMap<i32> = HashMap::new();
map.insert(CaseInsensitiveStr::new("Person"), 1);
map.insert(CaseInsensitiveStr::new("Company"), 2);
assert_eq!(map.get(&CaseInsensitiveStr::new("person")), Some(&1));
assert_eq!(map.get(&CaseInsensitiveStr::new("PERSON")), Some(&1));
assert_eq!(map.get(&CaseInsensitiveStr::new("Person")), Some(&1));
assert_eq!(map.get(&CaseInsensitiveStr::new("PeRsOn")), Some(&1));
assert_eq!(map.get(&CaseInsensitiveStr::new("company")), Some(&2));
assert_eq!(map.get(&CaseInsensitiveStr::new("COMPANY")), Some(&2));
assert_eq!(map.get(&CaseInsensitiveStr::new("Unknown")), None);
}
#[test]
fn test_case_insensitive_lookup_trait() {
let mut map = HashMap::new();
map.insert("Person".to_string(), 1);
map.insert("Company".to_string(), 2);
map.insert("fullName".to_string(), 3);
assert_eq!(map.get_ci("person"), Some(&1));
assert_eq!(map.get_ci("PERSON"), Some(&1));
assert_eq!(map.get_ci("Person"), Some(&1));
assert_eq!(map.get_ci("PeRsOn"), Some(&1));
assert_eq!(map.get_ci("company"), Some(&2));
assert_eq!(map.get_ci("COMPANY"), Some(&2));
assert_eq!(map.get_ci("fullname"), Some(&3));
assert_eq!(map.get_ci("FULLNAME"), Some(&3));
assert_eq!(map.get_ci("FullName"), Some(&3));
assert_eq!(map.get_ci("Unknown"), None);
assert!(map.contains_key_ci("person"));
assert!(map.contains_key_ci("COMPANY"));
assert!(map.contains_key_ci("FullName"));
assert!(!map.contains_key_ci("Unknown"));
}
#[test]
fn test_case_insensitive_lookup_exact_match_fast_path() {
let mut map = HashMap::new();
map.insert("Person".to_string(), 1);
assert_eq!(map.get_ci("Person"), Some(&1));
assert_eq!(map.get_ci("person"), Some(&1));
assert_eq!(map.get_ci("PERSON"), Some(&1));
}
#[test]
fn test_case_insensitive_str_as_str() {
let s = CaseInsensitiveStr::new("Person");
assert_eq!(s.as_str(), "person"); }
#[test]
fn test_case_insensitive_str_from_string() {
let s = String::from("Person");
let ci_str: CaseInsensitiveStr = s.into();
assert_eq!(ci_str.as_str(), "person");
}
#[test]
fn test_case_insensitive_str_from_str() {
let ci_str: CaseInsensitiveStr = "Person".into();
assert_eq!(ci_str.as_str(), "person");
}
#[test]
fn test_case_insensitive_map_insertion_deduplication() {
let mut map: CaseInsensitiveMap<i32> = HashMap::new();
map.insert(CaseInsensitiveStr::new("Person"), 1);
map.insert(CaseInsensitiveStr::new("person"), 2);
map.insert(CaseInsensitiveStr::new("PERSON"), 3);
assert_eq!(map.len(), 1);
assert_eq!(map.get(&CaseInsensitiveStr::new("person")), Some(&3));
}
#[test]
fn test_get_mut_ci() {
let mut map = HashMap::new();
map.insert("Person".to_string(), 1);
map.insert("Company".to_string(), 2);
if let Some(v) = map.get_mut_ci("person") {
*v = 10;
}
assert_eq!(map.get_ci("Person"), Some(&10));
if let Some(v) = map.get_mut_ci("COMPANY") {
*v = 20;
}
assert_eq!(map.get_ci("company"), Some(&20));
assert!(map.get_mut_ci("Unknown").is_none());
}
#[test]
fn test_property_name_normalization() {
let mut map = HashMap::new();
map.insert("fullName".to_string(), 1);
map.insert("isActive".to_string(), 2);
map.insert("numFollowers".to_string(), 3);
assert_eq!(map.get_ci("fullname"), Some(&1));
assert_eq!(map.get_ci("FULLNAME"), Some(&1));
assert_eq!(map.get_ci("FullName"), Some(&1));
assert_eq!(map.get_ci("isactive"), Some(&2));
assert_eq!(map.get_ci("ISACTIVE"), Some(&2));
assert_eq!(map.get_ci("IsActive"), Some(&2));
assert_eq!(map.get_ci("numfollowers"), Some(&3));
assert_eq!(map.get_ci("NUMFOLLOWERS"), Some(&3));
assert_eq!(map.get_ci("NumFollowers"), Some(&3));
}
#[test]
fn test_qualify_column() {
use super::qualify_column;
assert_eq!(qualify_column("p", "name"), "p__name");
assert_eq!(qualify_column("person", "age"), "person__age");
assert_eq!(qualify_column("P", "Name"), "p__name");
assert_eq!(qualify_column("PERSON", "AGE"), "person__age");
assert_eq!(qualify_column("Person", "fullName"), "person__fullname");
assert_eq!(qualify_column("MyVar", "IsActive"), "myvar__isactive");
assert_eq!(qualify_column("a", "NumFollowers"), "a__numfollowers");
}
}