Repository: rust-phf/rust-phf
Pull request #197 (2020-11-06 16:15:43)

Add support for uncased
I suspect the `unsafe` in this PR may make this unmergeable, but figured I'd throw up anyway, because the current support for `UniCase` is sadly pretty useless, as far as I can tell. Details below, but basically AFAICT you can only perform lookups in a `UniCase` map if you have a static string literal.

----

uncased is an ASCII-only case insensitive comparison library like
UniCase, but it has better support for being used in a map.

Specifically, UniCase does not support the Borrow trait (see
seanmonstar/unicase#22), so the type

    phf::Map<UniCase<&'static str>, _>

only supports lookups with static string literals, which has limited
usefulness.

By contrast, the the equivalent uncased construction

    phf::Map<&'static Uncased, _>

*does* support lookups with non-static strings.

benesch: Ok, done!
benesch: Hrm, seems like this might require an MSRV bump?

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b494040..e131097 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,10 +13,10 @@ jobs:
       fail-fast: false
       matrix:
         # MSRV and nightly
-        version: [1.40.0, nightly]
+        version: [1.46.0, nightly]
     steps:
       - uses: actions/checkout@v2
-        
+
       - name: Set toolchain
         run: |
           rustup set profile minimal
diff --git a/phf/Cargo.toml b/phf/Cargo.toml
index 8da827d..0ce6308 100644
--- a/phf/Cargo.toml
+++ b/phf/Cargo.toml
@@ -15,6 +15,7 @@ test = false
 [features]
 default = ["std"]
 std = ["phf_shared/std"]
+uncased = ["phf_shared/uncased"]
 unicase = ["phf_shared/unicase"]
 macros = [
 	"phf_macros",
diff --git a/phf_shared/Cargo.toml b/phf_shared/Cargo.toml
index bed2195..59dc717 100644
--- a/phf_shared/Cargo.toml
+++ b/phf_shared/Cargo.toml
@@ -19,3 +19,4 @@ std = []
 [dependencies]
 siphasher = "0.3"
 unicase = { version = "2.4.0", optional = true }
+uncased = { version = "0.9.6", optional = true, default-features = false }
diff --git a/phf_shared/src/lib.rs b/phf_shared/src/lib.rs
index de51c88..02dca59 100644
--- a/phf_shared/src/lib.rs
+++ b/phf_shared/src/lib.rs
@@ -276,6 +276,33 @@ impl<'b, 'a: 'b, S: ?Sized + 'a> PhfBorrow<unicase::UniCase<&'b S>> for unicase:
     }
 }
 
+#[cfg(feature = "uncased")]
+impl PhfHash for uncased::UncasedStr {
+    #[inline]
+    fn phf_hash<H: Hasher>(&self, state: &mut H) {
+        self.hash(state)
+    }
+}
+
+#[cfg(feature = "uncased")]
+impl FmtConst for uncased::UncasedStr {
+    fn fmt_const(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        // transmute is not stable in const fns (rust-lang/rust#53605), so
+        // `UncasedStr::new` can't be a const fn itself, but we can inline the
+        // call to transmute here in the meantime.
+        f.write_str("unsafe { ::std::mem::transmute::<&'static str, &'static UncasedStr>(")?;
+        self.as_str().fmt_const(f)?;
+        f.write_str(") }")
+    }
+}
+
+#[cfg(feature = "uncased")]
+impl PhfBorrow<uncased::UncasedStr> for &uncased::UncasedStr {
+    fn borrow(&self) -> &uncased::UncasedStr {
+        self
+    }
+}
+
 macro_rules! sip_impl (
     (le $t:ty) => (
         impl PhfHash for $t {


Test patch:
diff --git a/phf_codegen/test/Cargo.toml b/phf_codegen/test/Cargo.toml
index c8acbbd..9499fcb 100644
--- a/phf_codegen/test/Cargo.toml
+++ b/phf_codegen/test/Cargo.toml
@@ -6,9 +6,11 @@ build = "build.rs"
 edition = "2018"
 
 [dependencies]
-phf = { version = "0.8", features = ["unicase"] }
+phf = { version = "0.8", features = ["uncased", "unicase"] }
+uncased = { version = "0.9.6", default-features = false }
 unicase = "2.4.0"
 
 [build-dependencies]
 phf_codegen = "0.8"
 unicase = "2.4.0"
+uncased = { version = "0.9.6", default-features = false }
diff --git a/phf_codegen/test/build.rs b/phf_codegen/test/build.rs
index 14078ab..593ab98 100644
--- a/phf_codegen/test/build.rs
+++ b/phf_codegen/test/build.rs
@@ -6,6 +6,7 @@ use std::fs::File;
 use std::io::{self, BufWriter, Write};
 use std::path::Path;
 
+use uncased::UncasedStr;
 use unicase::UniCase;
 
 fn main() -> io::Result<()> {
@@ -71,6 +72,15 @@ fn main() -> io::Result<()> {
             .build()
     )?;
 
+    write!(
+        &mut file,
+        "static UNCASED_MAP: ::phf::Map<&'static ::uncased::UncasedStr, &'static str> = \n{};",
+        phf_codegen::Map::new()
+            .entry(UncasedStr::new("abc"), "\"a\"")
+            .entry(UncasedStr::new("DEF"), "\"b\"")
+            .build()
+    )?;
+
     //u32 is used here purely for a type that impls `Hash+PhfHash+Eq+fmt::Debug`, but is not required for the empty test itself
     writeln!(&mut file,
              "static EMPTY: ::phf::Map<u32, u32> = \n{};",
diff --git a/phf_codegen/test/src/lib.rs b/phf_codegen/test/src/lib.rs
index b62605c..3398d48 100644
--- a/phf_codegen/test/src/lib.rs
+++ b/phf_codegen/test/src/lib.rs
@@ -1,5 +1,6 @@
 #[cfg(test)]
 mod test {
+    use uncased::UncasedStr;
     use unicase::UniCase;
 
     include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
@@ -61,6 +62,14 @@ mod test {
         assert_eq!("b", UNICASE_MAP[&UniCase::new(&*local_str_3)]);
     }
 
+    #[test]
+    fn uncased_map() {
+        assert_eq!("a", UNCASED_MAP[&UncasedStr::new("AbC")]);
+        assert_eq!("a", UNCASED_MAP[&UncasedStr::new("abc")]);
+        assert_eq!("b", UNCASED_MAP[&UncasedStr::new("DEf")]);
+        assert!(!UNCASED_MAP.contains_key(&UncasedStr::new("XyZ")));
+    }
+
     #[test]
     fn array_keys() {
         assert_eq!(0, ARRAY_KEYS[b"foo"]);