Repository: rust-phf/rust-phf (rust)

Issue:
Support for tuples
I would like to be able to make a set of tuples, like so:

```rust
use phf::phf_set;
static MY_SET: phf::Set<(&str, u32, &[&str])> = phf_set! {
    ("some value", 1, &["even", "more", "values"]),
};
```
This is currently not possible it seems. I am guessing it's also not possible to use tuples as keys in phf::Map

You can work around this by using phf_map with the first value as key and the rest as the resulting value but that isn't very ergonomic and also only works if the first value is unique.

Pull request description:
feat: Add tuple support for key
Closes #198


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9ab8e65..969118a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,7 +40,7 @@ jobs:
 
       - name: trybuild test
         if: matrix.version == 'stable'
-        run: cargo test -p phf_macros_tests -- --ignored
+        run: cargo test -p phf_macros_test -- --ignored
 
       - name: no_std build check
         working-directory: phf
diff --git a/Cargo.toml b/Cargo.toml
index c5f53fb..b936828 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,7 @@ members = [
     "phf_codegen/test",
     "phf_generator",
     "phf_macros",
-    "phf_macros_tests",
+    "phf_macros_test",
     "phf_shared"
 ]
 
diff --git a/phf_macros/src/lib.rs b/phf_macros/src/lib.rs
index 871bda5..e502a70 100644
--- a/phf_macros/src/lib.rs
+++ b/phf_macros/src/lib.rs
@@ -35,6 +35,7 @@ enum ParsedKey {
     U128(u128),
     Usize(usize),
     Bool(bool),
+    Tuple(Vec<ParsedKey>),
     #[cfg(feature = "unicase")]
     UniCase(UniCase<String>),
     #[cfg(feature = "unicase")]
@@ -65,6 +66,11 @@ impl PhfHash for ParsedKey {
             ParsedKey::U128(s) => s.phf_hash(state),
             ParsedKey::Usize(s) => s.phf_hash(state),
             ParsedKey::Bool(s) => s.phf_hash(state),
+            ParsedKey::Tuple(elements) => {
+                for element in elements {
+                    element.phf_hash(state);
+                }
+            }
             #[cfg(feature = "unicase")]
             ParsedKey::UniCase(s) => s.phf_hash(state),
             #[cfg(feature = "unicase")]
@@ -99,6 +105,14 @@ impl ParsedKey {
                     "u64" => Some(ParsedKey::U64(s.base10_parse::<u64>().unwrap())),
                     "u128" => Some(ParsedKey::U128(s.base10_parse::<u128>().unwrap())),
                     "usize" => Some(ParsedKey::Usize(s.base10_parse::<usize>().unwrap())),
+                    // Handle unsuffixed integer literals, default to i32
+                    "" => {
+                        if let Ok(val) = s.base10_parse::<i32>() {
+                            Some(ParsedKey::I32(val))
+                        } else {
+                            None
+                        }
+                    }
                     _ => None,
                 },
                 Lit::Bool(s) => Some(ParsedKey::Bool(s.value)),
@@ -161,6 +175,17 @@ impl ParsedKey {
                     _ => None,
                 }
             }
+            Expr::Tuple(tuple) => {
+                let mut elements = Vec::new();
+                for elem in &tuple.elems {
+                    if let Some(parsed_elem) = ParsedKey::from_expr(elem) {
+                        elements.push(parsed_elem);
+                    } else {
+                        return None;
+                    }
+                }
+                Some(ParsedKey::Tuple(elements))
+            }
             Expr::Group(group) => ParsedKey::from_expr(&group.expr),
             Expr::Call(call) if call.args.len() == 1 => {
                 let last;
diff --git a/phf_macros_tests/tests/trybuild.rs b/phf_macros_test/tests/trybuild.rs
similarity index 100%
rename from phf_macros_tests/tests/trybuild.rs
rename to phf_macros_test/tests/trybuild.rs
diff --git a/phf_shared/src/lib.rs b/phf_shared/src/lib.rs
index 0495344..b7b9cba 100644
--- a/phf_shared/src/lib.rs
+++ b/phf_shared/src/lib.rs
@@ -473,3 +473,52 @@ slice_impl!(u128);
 slice_impl!(i128);
 slice_impl!(bool);
 slice_impl!(char);
+
+macro_rules! tuple_impl {
+    ($($t:ident),+) => {
+        impl<$($t: PhfHash),+> PhfHash for ($($t,)+) {
+            fn phf_hash<HS: Hasher>(&self, state: &mut HS) {
+                #[allow(non_snake_case)]
+                let ($($t,)+) = self;
+                $(
+                    $t.phf_hash(state);
+                )+
+            }
+        }
+
+        impl<$($t: PhfHash),+> PhfBorrow<($($t,)+)> for ($($t,)+) {
+            fn borrow(&self) -> &($($t,)+) {
+                self
+            }
+        }
+
+        impl<$($t: FmtConst),+> FmtConst for ($($t,)+) {
+            fn fmt_const(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                #[allow(non_snake_case)]
+                let ($($t,)+) = self;
+                write!(f, "(")?;
+                let mut first = true;
+                $(
+                    if !core::mem::replace(&mut first, false) {
+                        write!(f, ", ")?;
+                    }
+                    $t.fmt_const(f)?;
+                )+
+                write!(f, ")")
+            }
+        }
+    };
+}
+
+tuple_impl!(A);
+tuple_impl!(A, B);
+tuple_impl!(A, B, C);
+tuple_impl!(A, B, C, D);
+tuple_impl!(A, B, C, D, E);
+tuple_impl!(A, B, C, D, E, F);
+tuple_impl!(A, B, C, D, E, F, G);
+tuple_impl!(A, B, C, D, E, F, G, HT);
+tuple_impl!(A, B, C, D, E, F, G, HT, I);
+tuple_impl!(A, B, C, D, E, F, G, HT, I, J);
+tuple_impl!(A, B, C, D, E, F, G, HT, I, J, K);
+tuple_impl!(A, B, C, D, E, F, G, HT, I, J, K, L);


Test patch:
diff --git a/phf_codegen/test/build.rs b/phf_codegen/test/build.rs
index 6cd3585..8dc11f3 100644
--- a/phf_codegen/test/build.rs
+++ b/phf_codegen/test/build.rs
@@ -130,5 +130,49 @@ fn main() -> io::Result<()> {
             .into_iter()
             .collect::<phf_codegen::Map<_>>()
             .build()
+    )?;
+
+    // Test tuple keys for Map
+    writeln!(
+        &mut file,
+        "static TUPLE_MAP: ::phf::Map<(u32, &'static str), &'static str> = \n{};",
+        phf_codegen::Map::new()
+            .entry((1u32, "a"), "\"first\"")
+            .entry((2u32, "b"), "\"second\"")
+            .entry((3u32, "c"), "\"third\"")
+            .build()
+    )?;
+
+    // Test tuple keys for Set
+    writeln!(
+        &mut file,
+        "static TUPLE_SET: ::phf::Set<(u32, &'static str)> = \n{};",
+        phf_codegen::Set::new()
+            .entry((1u32, "x"))
+            .entry((2u32, "y"))
+            .entry((3u32, "z"))
+            .build()
+    )?;
+
+    // Test nested tuple keys for Map
+    writeln!(
+        &mut file,
+        "static NESTED_TUPLE_MAP: ::phf::Map<((u32, u32), &'static str), u32> = \n{};",
+        phf_codegen::Map::new()
+            .entry(((1u32, 2u32), "nested"), "10")
+            .entry(((3u32, 4u32), "tuple"), "20")
+            .entry(((5u32, 6u32), "keys"), "30")
+            .build()
+    )?;
+
+    // Test mixed type tuple keys
+    writeln!(
+        &mut file,
+        "static MIXED_TUPLE_MAP: ::phf::Map<(bool, u8, &'static str), &'static str> = \n{};",
+        phf_codegen::Map::new()
+            .entry((true, 1u8, "test"), "\"value1\"")
+            .entry((false, 2u8, "demo"), "\"value2\"")
+            .entry((true, 3u8, "example"), "\"value3\"")
+            .build()
     )
 }
diff --git a/phf_codegen/test/src/lib.rs b/phf_codegen/test/src/lib.rs
index 9778307..6a7ed17 100644
--- a/phf_codegen/test/src/lib.rs
+++ b/phf_codegen/test/src/lib.rs
@@ -125,4 +125,40 @@ mod test {
         assert_eq!(3, FROM_ITER_MAP["three"]);
         assert!(!FROM_ITER_MAP.contains_key("four"));
     }
+
+    #[test]
+    fn tuple_map() {
+        assert_eq!("first", TUPLE_MAP[&(1u32, "a")]);
+        assert_eq!("second", TUPLE_MAP[&(2u32, "b")]);
+        assert_eq!("third", TUPLE_MAP[&(3u32, "c")]);
+        assert!(!TUPLE_MAP.contains_key(&(4u32, "d")));
+        assert!(!TUPLE_MAP.contains_key(&(1u32, "b")));
+    }
+
+    #[test]
+    fn tuple_set() {
+        assert!(TUPLE_SET.contains(&(1u32, "x")));
+        assert!(TUPLE_SET.contains(&(2u32, "y")));
+        assert!(TUPLE_SET.contains(&(3u32, "z")));
+        assert!(!TUPLE_SET.contains(&(4u32, "w")));
+        assert!(!TUPLE_SET.contains(&(1u32, "y")));
+    }
+
+    #[test]
+    fn nested_tuple_map() {
+        assert_eq!(10, NESTED_TUPLE_MAP[&((1u32, 2u32), "nested")]);
+        assert_eq!(20, NESTED_TUPLE_MAP[&((3u32, 4u32), "tuple")]);
+        assert_eq!(30, NESTED_TUPLE_MAP[&((5u32, 6u32), "keys")]);
+        assert!(!NESTED_TUPLE_MAP.contains_key(&((7u32, 8u32), "missing")));
+        assert!(!NESTED_TUPLE_MAP.contains_key(&((1u32, 2u32), "wrong")));
+    }
+
+    #[test]
+    fn mixed_tuple_map() {
+        assert_eq!("value1", MIXED_TUPLE_MAP[&(true, 1u8, "test")]);
+        assert_eq!("value2", MIXED_TUPLE_MAP[&(false, 2u8, "demo")]);
+        assert_eq!("value3", MIXED_TUPLE_MAP[&(true, 3u8, "example")]);
+        assert!(!MIXED_TUPLE_MAP.contains_key(&(false, 1u8, "test")));
+        assert!(!MIXED_TUPLE_MAP.contains_key(&(true, 4u8, "missing")));
+    }
 }
diff --git a/phf_macros_tests/Cargo.toml b/phf_macros_test/Cargo.toml
similarity index 95%
rename from phf_macros_tests/Cargo.toml
rename to phf_macros_test/Cargo.toml
index ee363c6..d978a8e 100644
--- a/phf_macros_tests/Cargo.toml
+++ b/phf_macros_test/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "phf_macros_tests"
+name = "phf_macros_test"
 version = "0.1.0"
 authors = ["Yuki Okushi"]
 edition = "2021"
diff --git a/phf_macros_tests/benches/bench.rs b/phf_macros_test/benches/bench.rs
similarity index 100%
rename from phf_macros_tests/benches/bench.rs
rename to phf_macros_test/benches/bench.rs
diff --git a/phf_macros_tests/src/lib.rs b/phf_macros_test/src/lib.rs
similarity index 100%
rename from phf_macros_tests/src/lib.rs
rename to phf_macros_test/src/lib.rs
diff --git a/phf_macros_tests/tests/compile-fail-uncased/equivalent-keys.rs b/phf_macros_test/tests/compile-fail-uncased/equivalent-keys.rs
similarity index 100%
rename from phf_macros_tests/tests/compile-fail-uncased/equivalent-keys.rs
rename to phf_macros_test/tests/compile-fail-uncased/equivalent-keys.rs
diff --git a/phf_macros_tests/tests/compile-fail-uncased/equivalent-keys.stderr b/phf_macros_test/tests/compile-fail-uncased/equivalent-keys.stderr
similarity index 100%
rename from phf_macros_tests/tests/compile-fail-uncased/equivalent-keys.stderr
rename to phf_macros_test/tests/compile-fail-uncased/equivalent-keys.stderr
diff --git a/phf_macros_tests/tests/compile-fail-unicase/equivalent-keys.rs b/phf_macros_test/tests/compile-fail-unicase/equivalent-keys.rs
similarity index 100%
rename from phf_macros_tests/tests/compile-fail-unicase/equivalent-keys.rs
rename to phf_macros_test/tests/compile-fail-unicase/equivalent-keys.rs
diff --git a/phf_macros_tests/tests/compile-fail-unicase/equivalent-keys.stderr b/phf_macros_test/tests/compile-fail-unicase/equivalent-keys.stderr
similarity index 100%
rename from phf_macros_tests/tests/compile-fail-unicase/equivalent-keys.stderr
rename to phf_macros_test/tests/compile-fail-unicase/equivalent-keys.stderr
diff --git a/phf_macros_tests/tests/compile-fail/bad-syntax.rs b/phf_macros_test/tests/compile-fail/bad-syntax.rs
similarity index 100%
rename from phf_macros_tests/tests/compile-fail/bad-syntax.rs
rename to phf_macros_test/tests/compile-fail/bad-syntax.rs
diff --git a/phf_macros_tests/tests/compile-fail/bad-syntax.stderr b/phf_macros_test/tests/compile-fail/bad-syntax.stderr
similarity index 100%
rename from phf_macros_tests/tests/compile-fail/bad-syntax.stderr
rename to phf_macros_test/tests/compile-fail/bad-syntax.stderr
diff --git a/phf_macros_tests/tests/compile-fail/mixed.rs b/phf_macros_test/tests/compile-fail/mixed.rs
similarity index 100%
rename from phf_macros_tests/tests/compile-fail/mixed.rs
rename to phf_macros_test/tests/compile-fail/mixed.rs
diff --git a/phf_macros_tests/tests/compile-fail/mixed.stderr b/phf_macros_test/tests/compile-fail/mixed.stderr
similarity index 77%
rename from phf_macros_tests/tests/compile-fail/mixed.stderr
rename to phf_macros_test/tests/compile-fail/mixed.stderr
index 9943b2b..dd1246c 100644
--- a/phf_macros_tests/tests/compile-fail/mixed.stderr
+++ b/phf_macros_test/tests/compile-fail/mixed.stderr
@@ -23,11 +23,11 @@ error[E0277]: the trait bound `UniCase<&str>: phf_shared::PhfBorrow<_>` is not s
              `&[u8; N]` implements `phf_shared::PhfBorrow<[u8; N]>`
              `&[u8]` implements `phf_shared::PhfBorrow<[u8]>`
              `&str` implements `phf_shared::PhfBorrow<str>`
-             `String` implements `phf_shared::PhfBorrow<str>`
-             `Vec<u8>` implements `phf_shared::PhfBorrow<[u8]>`
-             `[bool; N]` implements `phf_shared::PhfBorrow<[bool]>`
-             `[char; N]` implements `phf_shared::PhfBorrow<[char]>`
-             `[i128; N]` implements `phf_shared::PhfBorrow<[i128]>`
+             `(A, B)` implements `phf_shared::PhfBorrow<(A, B)>`
+             `(A, B, C)` implements `phf_shared::PhfBorrow<(A, B, C)>`
+             `(A, B, C, D)` implements `phf_shared::PhfBorrow<(A, B, C, D)>`
+             `(A, B, C, D, E)` implements `phf_shared::PhfBorrow<(A, B, C, D, E)>`
+             `(A, B, C, D, E, F)` implements `phf_shared::PhfBorrow<(A, B, C, D, E, F)>`
            and $N others
 note: required by a bound in `phf::Map::<K, V>::get`
   --> $WORKSPACE/phf/src/map.rs
diff --git a/phf_macros_tests/tests/test.rs b/phf_macros_test/tests/test.rs
similarity index 92%
rename from phf_macros_tests/tests/test.rs
rename to phf_macros_test/tests/test.rs
index ee33cb5..f775cc5 100644
--- a/phf_macros_tests/tests/test.rs
+++ b/phf_macros_test/tests/test.rs
@@ -329,6 +329,19 @@ mod map {
         #[cfg(not(feature = "enabled_feature"))]
         assert_eq!(None, MY_MAP.get("baz"));
     }
+
+    #[test]
+    fn test_tuples() {
+        static MAP: phf::Map<(u32, &str), u32> = phf_map! {
+            (0, "a") => 1,
+            (1, "b") => 2,
+            (2, "c") => 3,
+        };
+        assert_eq!(Some(&1), MAP.get(&(0, "a")));
+        assert_eq!(Some(&2), MAP.get(&(1, "b")));
+        assert_eq!(Some(&3), MAP.get(&(2, "c")));
+        assert_eq!(None, MAP.get(&(3, "d")));
+    }
 }
 
 mod set {
@@ -408,6 +421,19 @@ mod set {
         #[cfg(not(feature = "enabled_feature"))]
         assert!(!SET.contains("baz"));
     }
+
+    #[test]
+    fn test_tuples() {
+        static SET: phf::Set<(u32, &str)> = phf_set! {
+            (0, "a"),
+            (1, "b"),
+            (2, "c"),
+        };
+        assert!(SET.contains(&(0, "a")));
+        assert!(SET.contains(&(1, "b")));
+        assert!(SET.contains(&(2, "c")));
+        assert!(!SET.contains(&(3, "d")));
+    }
 }
 
 mod ordered_map {
@@ -551,6 +577,19 @@ mod ordered_map {
         #[cfg(not(feature = "enabled_feature"))]
         assert_eq!(None, MY_MAP.get("baz"));
     }
+
+    #[test]
+    fn test_tuples() {
+        static MAP: phf::OrderedMap<(u32, &str), u32> = phf_ordered_map! {
+            (0, "a") => 1,
+            (1, "b") => 2,
+            (2, "c") => 3,
+        };
+        assert_eq!(Some(&1), MAP.get(&(0, "a")));
+        assert_eq!(Some(&2), MAP.get(&(1, "b")));
+        assert_eq!(Some(&3), MAP.get(&(2, "c")));
+        assert_eq!(None, MAP.get(&(3, "d")));
+    }
 }
 
 mod ordered_set {
@@ -652,4 +691,17 @@ mod ordered_set {
         #[cfg(not(feature = "enabled_feature"))]
         assert!(!SET.contains("baz"));
     }
+
+    #[test]
+    fn test_tuples() {
+        static SET: phf::OrderedSet<(u32, &str)> = phf_ordered_set! {
+            (0, "a"),
+            (1, "b"),
+            (2, "c"),
+        };
+        assert!(SET.contains(&(0, "a")));
+        assert!(SET.contains(&(1, "b")));
+        assert!(SET.contains(&(2, "c")));
+        assert!(!SET.contains(&(3, "d")));
+    }
 }