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

Issue:
Macros: key expressions should support match-like alternation with `|`
Instead of:
```rs
'a' => foo,
'b' => foo,
'c' => foo,
'd' => bar,
'e' => bar
```
, one should be able to write:
```rs
'a'|'b'|'c' => foo,
'd'|'e' => bar
```

Pull request description:
feat: Support OR pattern key
Close #286

Patch:
diff --git a/README.md b/README.md
index c53ffa8..cccafdb 100644
--- a/README.md
+++ b/README.md
@@ -51,9 +51,20 @@ static KEYWORDS: phf::Map<&'static str, Keyword> = phf_map! {
     "extern" => Keyword::Extern,
 };
 
+// You can also use OR (`|`) patterns to map multiple keys to the same value:
+static OPERATORS: phf::Map<&'static str, &'static str> = phf_map! {
+    "+" | "add" | "plus" => "addition",
+    "-" | "sub" | "minus" => "subtraction",
+    "*" | "mul" | "times" => "multiplication",
+};
+
 pub fn parse_keyword(keyword: &str) -> Option<Keyword> {
     KEYWORDS.get(keyword).cloned()
 }
+
+pub fn parse_operator(operator: &str) -> Option<&'static str> {
+    OPERATORS.get(operator).copied()
+}
 ```
 
 ```toml
@@ -103,6 +114,22 @@ fn main() {
     )
     .unwrap();
     write!(&mut file, ";\n").unwrap();
+
+    // Example with OR patterns (note: phf_codegen doesn't support OR patterns directly)
+    write!(
+        &mut file,
+        "static OPERATORS: phf::Map<&'static str, &'static str> = {}",
+        phf_codegen::Map::new()
+            .entry("+", "\"addition\"")
+            .entry("add", "\"addition\"")
+            .entry("plus", "\"addition\"")
+            .entry("-", "\"subtraction\"")
+            .entry("sub", "\"subtraction\"")
+            .entry("minus", "\"subtraction\"")
+            .build()
+    )
+    .unwrap();
+    write!(&mut file, ";\n").unwrap();
 }
 ```
 
@@ -123,4 +150,8 @@ include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
 pub fn parse_keyword(keyword: &str) -> Option<Keyword> {
     KEYWORDS.get(keyword).cloned()
 }
+
+pub fn parse_operator(operator: &str) -> Option<&'static str> {
+    OPERATORS.get(operator).copied()
+}
 ```
diff --git a/phf/src/lib.rs b/phf/src/lib.rs
index 6425e40..7db37d3 100644
--- a/phf/src/lib.rs
+++ b/phf/src/lib.rs
@@ -84,7 +84,9 @@ extern crate std as core;
 /// Supported key expressions are:
 /// - literals: bools, (byte) strings, bytes, chars, and integers (these must have a type suffix)
 /// - arrays of `u8` integers
+/// - tuples of any supported key expressions
 /// - dereferenced byte string literals
+/// - OR patterns using `|` to map multiple keys to the same value
 /// - `UniCase::unicode(string)`, `UniCase::ascii(string)`, or `Ascii::new(string)` if the `unicase` feature is enabled
 /// - `UncasedStr::new(string)` if the `uncased` feature is enabled
 ///
@@ -102,6 +104,26 @@ extern crate std as core;
 ///     assert_eq!(MY_MAP["hello"], 1);
 /// }
 /// ```
+///
+/// # OR Patterns
+///
+/// You can use OR patterns to map multiple keys to the same value:
+///
+/// ```
+/// use phf::{phf_map, Map};
+///
+/// static OPERATORS: Map<&'static str, &'static str> = phf_map! {
+///     "+" | "add" | "plus" => "addition",
+///     "-" | "sub" | "minus" => "subtraction",
+///     "*" | "mul" | "times" => "multiplication",
+/// };
+///
+/// fn main() {
+///     assert_eq!(OPERATORS["+"], "addition");
+///     assert_eq!(OPERATORS["add"], "addition");
+///     assert_eq!(OPERATORS["plus"], "addition");
+/// }
+/// ```
 pub use phf_macros::phf_map;
 
 #[cfg(feature = "macros")]
@@ -129,6 +151,27 @@ pub use phf_macros::phf_ordered_map;
 ///     assert!(MY_SET.contains("hello world"));
 /// }
 /// ```
+///
+/// # OR Patterns
+///
+/// You can use OR patterns to include multiple keys in a single entry:
+///
+/// ```
+/// use phf::{phf_set, Set};
+///
+/// static KEYWORDS: Set<&'static str> = phf_set! {
+///     "if" | "elif" | "else",
+///     "for" | "while" | "loop",
+///     "fn" | "function" | "def",
+/// };
+///
+/// fn main() {
+///     assert!(KEYWORDS.contains("if"));
+///     assert!(KEYWORDS.contains("elif"));
+///     assert!(KEYWORDS.contains("else"));
+///     assert!(KEYWORDS.contains("for"));
+/// }
+/// ```
 pub use phf_macros::phf_set;
 
 #[cfg(feature = "macros")]
diff --git a/phf_macros/src/lib.rs b/phf_macros/src/lib.rs
index e502a70..e68dc4c 100644
--- a/phf_macros/src/lib.rs
+++ b/phf_macros/src/lib.rs
@@ -11,7 +11,7 @@ use std::collections::HashSet;
 use std::hash::Hasher;
 use syn::parse::{self, Parse, ParseStream};
 use syn::punctuated::Punctuated;
-use syn::{parse_macro_input, Error, Expr, ExprLit, Lit, Token, UnOp};
+use syn::{parse_macro_input, BinOp, Error, Expr, ExprLit, Lit, Token, UnOp};
 #[cfg(feature = "uncased")]
 use uncased_::Uncased;
 #[cfg(feature = "unicase")]
@@ -234,8 +234,8 @@ impl ParsedKey {
 
 #[derive(Clone)]
 struct Key {
-    parsed: ParsedKey,
-    expr: Expr,
+    parsed: Vec<ParsedKey>,
+    expr: Vec<Expr>,
     attrs: Vec<syn::Attribute>,
 }
 
@@ -244,25 +244,63 @@ impl PhfHash for Key {
     where
         H: Hasher,
     {
-        self.parsed.phf_hash(state)
+        // For OR patterns, we hash the first key (they should all hash to the same value)
+        if let Some(first) = self.parsed.first() {
+            first.phf_hash(state);
+        }
     }
 }
 
 impl Parse for Key {
     fn parse(input: ParseStream<'_>) -> parse::Result<Key> {
         let attrs = input.call(syn::Attribute::parse_outer)?;
-        let expr = input.parse()?;
-        let parsed = ParsedKey::from_expr(&expr)
-            .ok_or_else(|| Error::new_spanned(&expr, "unsupported key expression"))?;
+
+        // Parse the expression (which might contain OR patterns)
+        let expr = input.parse::<Expr>()?;
+
+        // Extract all keys from the expression (handling OR patterns)
+        let (exprs, parsed_keys) = extract_keys_from_expr(&expr)?;
 
         Ok(Key {
-            parsed,
-            expr,
+            parsed: parsed_keys,
+            expr: exprs,
             attrs,
         })
     }
 }
 
+/// Extract all keys from an expression, handling OR patterns
+fn extract_keys_from_expr(expr: &Expr) -> parse::Result<(Vec<Expr>, Vec<ParsedKey>)> {
+    match expr {
+        Expr::Binary(binary) => {
+            if let BinOp::BitOr(_) = binary.op {
+                // Handle OR pattern: left | right
+                let (left_exprs, left_keys) = extract_keys_from_expr(&binary.left)?;
+                let (right_exprs, right_keys) = extract_keys_from_expr(&binary.right)?;
+
+                let mut exprs = left_exprs;
+                exprs.extend(right_exprs);
+
+                let mut keys = left_keys;
+                keys.extend(right_keys);
+
+                Ok((exprs, keys))
+            } else {
+                // Single key
+                let parsed = ParsedKey::from_expr(expr)
+                    .ok_or_else(|| Error::new_spanned(expr, "unsupported key expression"))?;
+                Ok((vec![expr.clone()], vec![parsed]))
+            }
+        }
+        _ => {
+            // Single key
+            let parsed = ParsedKey::from_expr(expr)
+                .ok_or_else(|| Error::new_spanned(expr, "unsupported key expression"))?;
+            Ok((vec![expr.clone()], vec![parsed]))
+        }
+    }
+}
+
 #[derive(Clone)]
 struct Entry {
     key: Key,
@@ -294,9 +332,41 @@ struct Map(Vec<Entry>);
 impl Parse for Map {
     fn parse(input: ParseStream<'_>) -> parse::Result<Map> {
         let parsed = Punctuated::<Entry, Token![,]>::parse_terminated(input)?;
-        let map = parsed.into_iter().collect::<Vec<_>>();
-        check_duplicates(&map)?;
-        Ok(Map(map))
+        let mut expanded_entries = Vec::new();
+
+        // Expand OR patterns into multiple entries
+        for entry in parsed {
+            for (i, (parsed_key, expr)) in entry
+                .key
+                .parsed
+                .iter()
+                .zip(entry.key.expr.iter())
+                .enumerate()
+            {
+                let expanded_key = Key {
+                    parsed: vec![parsed_key.clone()],
+                    expr: vec![expr.clone()],
+                    attrs: if i == 0 {
+                        entry.key.attrs.clone()
+                    } else {
+                        Vec::new()
+                    },
+                };
+                let expanded_entry = Entry {
+                    key: expanded_key,
+                    value: entry.value.clone(),
+                    attrs: if i == 0 {
+                        entry.attrs.clone()
+                    } else {
+                        Vec::new()
+                    },
+                };
+                expanded_entries.push(expanded_entry);
+            }
+        }
+
+        check_duplicates(&expanded_entries)?;
+        Ok(Map(expanded_entries))
     }
 }
 
@@ -307,29 +377,45 @@ impl Parse for Set {
         let parsed = Punctuated::<Key, Token![,]>::parse_terminated(input)?;
         let unit_value: Expr = syn::parse_str("()").expect("Failed to parse unit value");
 
-        let set: Vec<Entry> = parsed
-            .into_iter()
-            .map(|key| Entry {
-                key: Key {
-                    parsed: key.parsed.clone(),
-                    expr: key.expr.clone(),
-                    attrs: Vec::new(),
-                },
-                value: unit_value.clone(),
-                attrs: key.attrs,
-            })
-            .collect();
+        let mut expanded_entries = Vec::new();
+
+        // Expand OR patterns into multiple entries
+        for key in parsed {
+            for (i, (parsed_key, expr)) in key.parsed.iter().zip(key.expr.iter()).enumerate() {
+                let expanded_key = Key {
+                    parsed: vec![parsed_key.clone()],
+                    expr: vec![expr.clone()],
+                    attrs: if i == 0 {
+                        key.attrs.clone()
+                    } else {
+                        Vec::new()
+                    },
+                };
+                let expanded_entry = Entry {
+                    key: expanded_key,
+                    value: unit_value.clone(),
+                    attrs: if i == 0 {
+                        key.attrs.clone()
+                    } else {
+                        Vec::new()
+                    },
+                };
+                expanded_entries.push(expanded_entry);
+            }
+        }
 
-        check_duplicates(&set)?;
-        Ok(Set(set))
+        check_duplicates(&expanded_entries)?;
+        Ok(Set(expanded_entries))
     }
 }
 
 fn check_duplicates(entries: &[Entry]) -> parse::Result<()> {
     let mut keys = HashSet::new();
     for entry in entries {
-        if !keys.insert(&entry.key.parsed) {
-            return Err(Error::new_spanned(&entry.key.expr, "duplicate key"));
+        if let Some(first) = entry.key.parsed.first() {
+            if !keys.insert(first) {
+                return Err(Error::new_spanned(&entry.key.expr[0], "duplicate key"));
+            }
         }
     }
     Ok(())
@@ -340,7 +426,7 @@ fn build_map(entries: &[Entry], state: HashState) -> proc_macro2::TokenStream {
     let disps = state.disps.iter().map(|&(d1, d2)| quote!((#d1, #d2)));
     let entries = state.map.iter().map(|&idx| {
         let entry = &entries[idx];
-        let key = &entry.key.expr;
+        let key = &entry.key.expr[0]; // Use the first expression
         let value = &entry.value;
         // Don't include attributes since we've filtered at macro expansion time
         quote!((#key, #value))
@@ -360,7 +446,7 @@ fn build_ordered_map(entries: &[Entry], state: HashState) -> proc_macro2::TokenS
     let disps = state.disps.iter().map(|&(d1, d2)| quote!((#d1, #d2)));
     let idxs = state.map.iter().map(|idx| quote!(#idx));
     let entries = entries.iter().map(|entry| {
-        let key = &entry.key.expr;
+        let key = &entry.key.expr[0]; // Use the first expression
         let value = &entry.value;
         // Don't include attributes since we've filtered at macro expansion time
         quote!((#key, #value))


Test patch:
diff --git a/phf_macros_test/tests/test.rs b/phf_macros_test/tests/test.rs
index f775cc5..d949132 100644
--- a/phf_macros_test/tests/test.rs
+++ b/phf_macros_test/tests/test.rs
@@ -342,6 +342,32 @@ mod map {
         assert_eq!(Some(&3), MAP.get(&(2, "c")));
         assert_eq!(None, MAP.get(&(3, "d")));
     }
+
+    #[test]
+    fn test_or_pattern() {
+        static MAP: phf::Map<&'static str, isize> = phf_map!(
+            "foo" | "baz" => 10,
+            "bar" => 20,
+        );
+        assert_eq!(Some(&10), MAP.get("foo"));
+        assert_eq!(Some(&10), MAP.get("baz"));
+        assert_eq!(Some(&20), MAP.get("bar"));
+        assert_eq!(None, MAP.get("qux"));
+
+        // Test with three or more keys
+        static MAP2: phf::Map<&'static str, isize> = phf_map!(
+            "foo" | "baz" | "qux" => 10,
+            "bar" | "quux" => 20,
+            "xyz" => 30,
+        );
+        assert_eq!(Some(&10), MAP2.get("foo"));
+        assert_eq!(Some(&10), MAP2.get("baz"));
+        assert_eq!(Some(&10), MAP2.get("qux"));
+        assert_eq!(Some(&20), MAP2.get("bar"));
+        assert_eq!(Some(&20), MAP2.get("quux"));
+        assert_eq!(Some(&30), MAP2.get("xyz"));
+        assert_eq!(None, MAP2.get("unknown"));
+    }
 }
 
 mod set {
@@ -434,6 +460,34 @@ mod set {
         assert!(SET.contains(&(2, "c")));
         assert!(!SET.contains(&(3, "d")));
     }
+
+    #[test]
+    fn test_or_pattern() {
+        static SET: phf::Set<&'static str> = phf_set! {
+            "foo" | "baz",
+            "bar" | "qux",
+        };
+        assert!(SET.contains("foo"));
+        assert!(SET.contains("baz"));
+        assert!(SET.contains("bar"));
+        assert!(SET.contains("qux"));
+        assert!(!SET.contains("unknown"));
+        assert_eq!(4, SET.len());
+
+        // Test with three or more keys
+        static SET2: phf::Set<&'static str> = phf_set! {
+            "foo" | "baz" | "qux",
+            "bar" | "quux" | "xyz",
+        };
+        assert!(SET2.contains("foo"));
+        assert!(SET2.contains("baz"));
+        assert!(SET2.contains("qux"));
+        assert!(SET2.contains("bar"));
+        assert!(SET2.contains("quux"));
+        assert!(SET2.contains("xyz"));
+        assert!(!SET2.contains("unknown"));
+        assert_eq!(6, SET2.len());
+    }
 }
 
 mod ordered_map {
@@ -590,6 +644,34 @@ mod ordered_map {
         assert_eq!(Some(&3), MAP.get(&(2, "c")));
         assert_eq!(None, MAP.get(&(3, "d")));
     }
+
+    #[test]
+    fn test_or_pattern() {
+        static MAP: phf::OrderedMap<&'static str, isize> = phf_ordered_map!(
+            "foo" | "baz" => 10,
+            "bar" | "qux" => 20,
+        );
+        assert_eq!(Some(&10), MAP.get("foo"));
+        assert_eq!(Some(&10), MAP.get("baz"));
+        assert_eq!(Some(&20), MAP.get("bar"));
+        assert_eq!(Some(&20), MAP.get("qux"));
+        assert_eq!(None, MAP.get("unknown"));
+        assert_eq!(4, MAP.len());
+
+        // Test with three or more keys
+        static MAP2: phf::OrderedMap<&'static str, isize> = phf_ordered_map!(
+            "foo" | "baz" | "qux" => 10,
+            "bar" | "quux" | "xyz" => 20,
+        );
+        assert_eq!(Some(&10), MAP2.get("foo"));
+        assert_eq!(Some(&10), MAP2.get("baz"));
+        assert_eq!(Some(&10), MAP2.get("qux"));
+        assert_eq!(Some(&20), MAP2.get("bar"));
+        assert_eq!(Some(&20), MAP2.get("quux"));
+        assert_eq!(Some(&20), MAP2.get("xyz"));
+        assert_eq!(None, MAP2.get("unknown"));
+        assert_eq!(6, MAP2.len());
+    }
 }
 
 mod ordered_set {
@@ -704,4 +786,32 @@ mod ordered_set {
         assert!(SET.contains(&(2, "c")));
         assert!(!SET.contains(&(3, "d")));
     }
+
+    #[test]
+    fn test_or_pattern() {
+        static SET: phf::OrderedSet<&'static str> = phf_ordered_set! {
+            "foo" | "baz",
+            "bar" | "qux",
+        };
+        assert!(SET.contains("foo"));
+        assert!(SET.contains("baz"));
+        assert!(SET.contains("bar"));
+        assert!(SET.contains("qux"));
+        assert!(!SET.contains("unknown"));
+        assert_eq!(4, SET.len());
+
+        // Test with three or more keys
+        static SET2: phf::OrderedSet<&'static str> = phf_ordered_set! {
+            "foo" | "baz" | "qux",
+            "bar" | "quux" | "xyz",
+        };
+        assert!(SET2.contains("foo"));
+        assert!(SET2.contains("baz"));
+        assert!(SET2.contains("qux"));
+        assert!(SET2.contains("bar"));
+        assert!(SET2.contains("quux"));
+        assert!(SET2.contains("xyz"));
+        assert!(!SET2.contains("unknown"));
+        assert_eq!(6, SET2.len());
+    }
 }