diff --git a/CHANGELOG.md b/CHANGELOG.md index 658a80b..8925bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## Unreleased - ReleaseDate +### Added + +- `Index` implementations for `IdHashMap` and `IdOrdMap`, so an item can be looked up with `map[&key]` the way `std`'s `HashMap` and `BTreeMap` allow. Like those, indexing panics if the key is absent; use `get` for a non-panicking lookup. + ## [0.4.6] - 2026-07-21 ### Added diff --git a/crates/iddqd/src/id_hash_map/imp.rs b/crates/iddqd/src/id_hash_map/imp.rs index 5955a0f..ceeb445 100644 --- a/crates/iddqd/src/id_hash_map/imp.rs +++ b/crates/iddqd/src/id_hash_map/imp.rs @@ -18,6 +18,7 @@ use crate::{ use core::{ fmt, hash::{BuildHasher, Hash}, + ops::Index, }; use equivalent::Equivalent; @@ -1689,6 +1690,59 @@ impl Eq { } +/// Look up an item by its key. +/// +/// The `for<'k>` bound is required because the index operation borrows keys +/// from items for an unnamed lifetime, so the query type has to compare equal +/// to keys of any lifetime. That holds for the usual cases like `&str` keys +/// queried with `str`. +/// +/// # Panics +/// +/// Panics if no item with the given key is present. Use [`IdHashMap::get`] for +/// a non-panicking lookup. +/// +/// # Examples +/// +/// ``` +/// # #[cfg(feature = "default-hasher")] { +/// use iddqd::{IdHashItem, IdHashMap, id_upcast}; +/// +/// #[derive(Debug, PartialEq, Eq, Hash)] +/// struct Item { +/// id: String, +/// value: u32, +/// } +/// +/// impl IdHashItem for Item { +/// type Key<'a> = &'a str; +/// fn key(&self) -> Self::Key<'_> { +/// &self.id +/// } +/// id_upcast!(); +/// } +/// +/// let mut map = IdHashMap::new(); +/// map.insert_unique(Item { id: "foo".to_string(), value: 42 }).unwrap(); +/// +/// assert_eq!(map["foo"].value, 42); +/// # } +/// ``` +impl Index<&Q> for IdHashMap +where + T: IdHashItem, + Q: ?Sized + Hash + for<'k> Equivalent>, + S: Clone + BuildHasher, + A: Allocator, +{ + type Output = T; + + #[inline] + fn index(&self, key: &Q) -> &T { + self.get(key).expect("no entry found for key") + } +} + /// The `Extend` implementation overwrites duplicates. In the future, there will /// also be an `extend_unique` method that will return an error. /// diff --git a/crates/iddqd/src/id_ord_map/imp.rs b/crates/iddqd/src/id_ord_map/imp.rs index b533261..04d30b6 100644 --- a/crates/iddqd/src/id_ord_map/imp.rs +++ b/crates/iddqd/src/id_ord_map/imp.rs @@ -16,6 +16,7 @@ use crate::{ use core::{ fmt, hash::{BuildHasher, Hash}, + ops::Index, }; use equivalent::{Comparable, Equivalent}; @@ -1738,3 +1739,52 @@ impl FromIterator for IdOrdMap { map } } + +/// Look up an item by its key. +/// +/// The `for<'k>` bound is required because the index operation borrows keys +/// from items for an unnamed lifetime, so the query type has to compare against +/// keys of any lifetime. That holds for the usual cases like `&str` keys +/// queried with `str`. +/// +/// # Panics +/// +/// Panics if no item with the given key is present. Use [`IdOrdMap::get`] for a +/// non-panicking lookup. +/// +/// # Examples +/// +/// ``` +/// use iddqd::{IdOrdItem, IdOrdMap, id_upcast}; +/// +/// #[derive(Debug)] +/// struct Item { +/// id: String, +/// value: u32, +/// } +/// +/// impl IdOrdItem for Item { +/// type Key<'a> = &'a str; +/// fn key(&self) -> Self::Key<'_> { +/// &self.id +/// } +/// id_upcast!(); +/// } +/// +/// let mut map = IdOrdMap::new(); +/// map.insert_unique(Item { id: "foo".to_string(), value: 42 }).unwrap(); +/// +/// assert_eq!(map["foo"].value, 42); +/// ``` +impl Index<&Q> for IdOrdMap +where + T: IdOrdItem, + Q: ?Sized + for<'k> Comparable>, +{ + type Output = T; + + #[inline] + fn index(&self, key: &Q) -> &T { + self.get(key).expect("no entry found for key") + } +} diff --git a/crates/iddqd/tests/integration/id_hash_map.rs b/crates/iddqd/tests/integration/id_hash_map.rs index 063baec..373f8e0 100644 --- a/crates/iddqd/tests/integration/id_hash_map.rs +++ b/crates/iddqd/tests/integration/id_hash_map.rs @@ -33,6 +33,38 @@ impl IdHashItem for SimpleItem { id_upcast!(); } +#[test] +fn index_by_key() { + let mut map = IdHashMap::::make_new(); + map.insert_unique(SimpleItem { key: 1 }).unwrap(); + map.insert_unique(SimpleItem { key: 20 }).unwrap(); + + assert_eq!(map[&1].key, 1); + assert_eq!(map[&20].key, 20); +} + +#[test] +fn index_borrowed_key() { + let mut map = IdHashMap::::make_new(); + map.insert_unique(BorrowedItem { + key1: "foo", + key2: Cow::Borrowed(b"foo"), + key3: Path::new("foo"), + }) + .unwrap(); + + // The query type `str` is shorter-lived than the stored `&'static str` + // keys, which exercises the `for<'k>` bound on the `Index` impl. + assert_eq!(map["foo"].key1, "foo"); +} + +#[test] +#[should_panic(expected = "no entry found for key")] +fn index_missing_key_panics() { + let map = IdHashMap::::make_new(); + let _ = &map[&1]; +} + #[test] fn debug_impls() { let mut map = IdHashMap::::make_new(); diff --git a/crates/iddqd/tests/integration/id_ord_map.rs b/crates/iddqd/tests/integration/id_ord_map.rs index ae75adb..469687a 100644 --- a/crates/iddqd/tests/integration/id_ord_map.rs +++ b/crates/iddqd/tests/integration/id_ord_map.rs @@ -56,6 +56,34 @@ impl IdOrdItem for SimpleItem { id_upcast!(); } +#[test] +fn index_by_key() { + let mut map = IdOrdMap::::make_new(); + map.insert_unique(SimpleItem { key: 1 }).unwrap(); + map.insert_unique(SimpleItem { key: 20 }).unwrap(); + + assert_eq!(map[&1].key, 1); + assert_eq!(map[&20].key, 20); +} + +#[test] +fn index_borrowed_key() { + let map = id_ord_map! { + BorrowedItem { key1: "foo", key2: Cow::Borrowed(b"foo"), key3: Path::new("foo") }, + }; + + // The query type `str` is shorter-lived than the stored `&'static str` + // keys, which exercises the `for<'k>` bound on the `Index` impl. + assert_eq!(map["foo"].key1, "foo"); +} + +#[test] +#[should_panic(expected = "no entry found for key")] +fn index_missing_key_panics() { + let map = IdOrdMap::::make_new(); + let _ = &map[&1]; +} + #[test] fn debug_impls() { let mut map = IdOrdMap::::make_new();