iddqd/
lib.rs

1//! Maps where keys are borrowed from values.
2//!
3//! This crate consists of several map types, collectively called **ID maps**:
4//!
5//! - [`IdOrdMap`]: A B-Tree based map where keys are borrowed from values.
6//! - [`IdHashMap`]: A hash map where keys are borrowed from values.
7//! - [`BiHashMap`]: A bijective (1:1) hash map with two keys, borrowed from
8//!   values.
9//! - [`TriHashMap`]: A trijective (1:1:1) hash map with three keys, borrowed
10//!   from values.
11//!
12//! # Usage
13//!
14//! * Pick your ID map type.
15//! * Depending on the ID map type, implement [`IdOrdItem`], [`IdHashItem`],
16//!   [`BiHashItem`], or [`TriHashItem`] for your value type.
17//! * Store values in the ID map type.
18//!
19//! ## Features
20//!
21//! This crate was built out a practical need for map types, and addresses
22//! issues encountered using Rust's default map types in practice at Oxide.
23//!
24//! * Keys are retrieved from values, not stored separately from them. Separate
25//!   storage has been a recurring pain point in our codebases: if keys are
26//!   duplicated within values, it's proven to be hard to maintain consistency
27//!   between keys and values. This crate addresses that need.
28//! * Keys may be borrowed from values, which allows for more flexible
29//!   implementations. (They don't have to be borrowed, but they can be.)
30//! * There's no `insert` method; insertion must be through either
31//!   `insert_overwrite` or `insert_unique`. You must pick an insertion
32//!   behavior.
33//! * For hash maps, the default hasher is [`foldhash`], which is much faster
34//!   than SipHash. However, foldhash does *not provide the same level of HashDoS
35//!   resistance* as SipHash. If that is important to you, you can use a different
36//!   hasher. (Disable the `default-hasher` feature to require a hash
37//!   builder type parameter to be passed in.)
38//! * The serde implementations reject duplicate keys.
39//!
40//! We've also sometimes needed to index a set of data by more than one key, or
41//! perhaps map one key to another. For that purpose, this crate provides
42//! [`BiHashMap`] and [`TriHashMap`].
43//!
44//! * [`BiHashMap`] has two keys, and provides a bijection (1:1 relationship)
45//!   between the keys.
46//! * [`TriHashMap`] has three keys, and provides a trijection (1:1:1
47//!   relationship) between the keys.
48//!
49//! As a consequence of the general API structure, maps can have arbitrary
50//! non-key data associated with them as well.
51//!
52//! ## Examples
53//!
54//! An example for [`IdOrdMap`]:
55//!
56//! ```
57//! # #[cfg(feature = "std")] {
58//! use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
59//!
60//! #[derive(Debug)]
61//! struct User {
62//!     name: String,
63//!     age: u8,
64//! }
65//!
66//! // Implement IdOrdItem so the map knows how to get the key from the value.
67//! impl IdOrdItem for User {
68//!     // The key type can borrow from the value.
69//!     type Key<'a> = &'a str;
70//!
71//!     fn key(&self) -> Self::Key<'_> {
72//!         &self.name
73//!     }
74//!
75//!     id_upcast!();
76//! }
77//!
78//! let mut users = IdOrdMap::<User>::new();
79//!
80//! // You must pick an insertion behavior. insert_unique returns an error if
81//! // the key already exists.
82//! users.insert_unique(User { name: "Alice".to_string(), age: 30 }).unwrap();
83//! users.insert_unique(User { name: "Bob".to_string(), age: 35 }).unwrap();
84//!
85//! // Lookup by name:
86//! assert_eq!(users.get("Alice").unwrap().age, 30);
87//! assert_eq!(users.get("Bob").unwrap().age, 35);
88//!
89//! // Iterate over users:
90//! for user in &users {
91//!     println!("User {}: {}", user.name, user.age);
92//! }
93//! # }
94//! ```
95//!
96//! Keys don't have to be borrowed from the value. For smaller `Copy` types,
97//! it's recommended that you use owned keys. Here's an example of using
98//! [`IdOrdMap`] with a small integer key:
99//!
100//! ```
101//! # #[cfg(feature = "std")] {
102//! # use iddqd::{IdOrdMap, IdOrdItem, id_upcast};
103//! struct Record {
104//!     id: u32,
105//!     data: String,
106//! }
107//!
108//! impl IdOrdItem for Record {
109//!     // The key type is small, so an owned key is preferred.
110//!     type Key<'a> = u32;
111//!
112//!     fn key(&self) -> Self::Key<'_> {
113//!         self.id
114//!     }
115//!
116//!     id_upcast!();
117//! }
118//!
119//! // ...
120//! # }
121//! ```
122//!
123//! An example for [`IdHashMap`], showing a complex borrowed key. Here,
124//! "complex" means that the key is not a reference itself, but a struct that
125//! returns references to more than one field from the value.
126//!
127//! ```
128//! # #[cfg(feature = "default-hasher")] {
129//! use iddqd::{IdHashItem, id_hash_map, id_upcast};
130//!
131//! #[derive(Debug)]
132//! struct Artifact {
133//!     name: String,
134//!     version: String,
135//!     data: Vec<u8>,
136//! }
137//!
138//! // The key type is a borrowed form of the name and version. It needs to
139//! // implement `Eq + Hash`.
140//! #[derive(Eq, Hash, PartialEq)]
141//! struct ArtifactKey<'a> {
142//!     name: &'a str,
143//!     version: &'a str,
144//! }
145//!
146//! impl IdHashItem for Artifact {
147//!     // The key type can borrow from the value.
148//!     type Key<'a> = ArtifactKey<'a>;
149//!
150//!     fn key(&self) -> Self::Key<'_> {
151//!         ArtifactKey { name: &self.name, version: &self.version }
152//!     }
153//!
154//!     id_upcast!();
155//! }
156//!
157//! // Create a new `IdHashMap` with the given artifacts. This uses the
158//! // `id_hash_map!` macro that comes with iddqd.
159//! let artifacts = id_hash_map! {
160//!     Artifact {
161//!         name: "artifact1".to_owned(),
162//!         version: "1.0".to_owned(),
163//!         data: b"data1".to_vec(),
164//!     },
165//!     Artifact {
166//!         name: "artifact2".to_owned(),
167//!         version: "1.0".to_owned(),
168//!         data: b"data2".to_vec(),
169//!     },
170//! };
171//!
172//! // Look up artifacts by name and version.
173//! assert_eq!(
174//!     artifacts
175//!         .get(&ArtifactKey { name: "artifact1", version: "1.0" })
176//!         .unwrap()
177//!         .data,
178//!     b"data1",
179//! );
180//! # }
181//! ```
182//!
183//! For more examples, see the
184//! [examples](https://github.com/oxidecomputer/iddqd/tree/main/crates/iddqd/examples)
185//! and [extended
186//! examples](https://github.com/oxidecomputer/iddqd/tree/main/crates/iddqd-extended-examples/examples)
187//! directories.
188//!
189//! ### `Equivalent` and `Comparable`
190//!
191//! An important feature of the standard library's maps is that they allow any
192//! borrowed form of the key type to be used for lookups; for example, a
193//! `HashMap<String, T>` type can be looked up with a `&str` key. This is done
194//! through the [`Borrow`] trait.
195//!
196//! But the [`Borrow`] trait is a bit too restrictive for complex keys such as
197//! `ArtifactKey` above, requiring workarounds such as [dynamic
198//! dispatch](https://github.com/sunshowers-code/borrow-complex-key-example). To
199//! address this, the crates.io ecosystem has standardized on the [`Equivalent`]
200//! and [`Comparable`] traits as generalizations of `Borrow`. The map types in
201//! this crate require these traits.
202//!
203//! For a key type `T::Key<'_>` and a lookup type `L`:
204//!
205//! * The hash map types require `L: Hash + Equivalent<T::Key<'_>>`. Also, `L`
206//!   must hash in the same way as `T::Key<'_>`. Typically, this is done by
207//!   ensuring that enum variants and struct fields are in the same
208//!   order[^proptest].
209//! * [`IdOrdMap`] requires `L: Comparable<T::Key<'_>>`, which in turn requires
210//!   `Equivalent<T::Key<'_>>`. (There's no need for `L` to implement `Ord` or
211//!   `Eq` itself.)
212//!
213//! [^proptest]: We recommend that you test this with e.g. a property-based
214//!   test: see [this
215//!   example](https://github.com/sunshowers-code/borrow-complex-key-example/blob/a6f17699/src/lib.rs#L233).
216//!
217//! Continuing the `ArtifactKey` example from above, we can perform a lookup
218//! using a key of this owned form:
219//!
220//! ```
221//! # #[cfg(feature = "default-hasher")] {
222//! use equivalent::Equivalent;
223//! # use iddqd::{id_hash_map, IdHashItem, id_upcast};
224//! # #[derive(Debug)]
225//! # struct Artifact {
226//! #     name: String,
227//! #     version: String,
228//! #     data: Vec<u8>,
229//! # }
230//! # #[derive(Eq, Hash, PartialEq)]
231//! # struct ArtifactKey<'a> {
232//! #     name: &'a str,
233//! #     version: &'a str,
234//! # }
235//! # impl IdHashItem for Artifact {
236//! #     type Key<'a> = ArtifactKey<'a>;
237//! #     fn key(&self) -> Self::Key<'_> {
238//! #         ArtifactKey {
239//! #             name: &self.name,
240//! #             version: &self.version,
241//! #         }
242//! #     }
243//! #     id_upcast!();
244//! # }
245//! # let artifacts = id_hash_map! {
246//! #     Artifact {
247//! #         name: "artifact1".to_owned(),
248//! #         version: "1.0".to_owned(),
249//! #         data: b"data1".to_vec(),
250//! #     }
251//! # };
252//!
253//! // This is an owned form of ArtifactKey. The fields are in the same
254//! // order as ArtifactKey's fields, so it hashes the same way.
255//! #[derive(Hash)]
256//! struct OwnedArtifactKey {
257//!     name: String,
258//!     version: String,
259//! }
260//!
261//! impl Equivalent<ArtifactKey<'_>> for OwnedArtifactKey {
262//!     fn equivalent(&self, other: &ArtifactKey<'_>) -> bool {
263//!         self.name == other.name && self.version == other.version
264//!     }
265//! }
266//!
267//! // Now you can use OwnedArtifactKey to look up the artifact.
268//! let owned_key = OwnedArtifactKey {
269//!     name: "artifact1".to_owned(),
270//!     version: "1.0".to_owned(),
271//! };
272//! assert_eq!(artifacts.get(&owned_key).unwrap().data, b"data1",);
273//! # }
274//! ```
275//!
276//! There's a blanket implementation of [`Equivalent`] and [`Comparable`] for
277//! [`Borrow`], so if your type already implements [`Borrow`], there aren't any
278//! extra steps to take.
279//!
280//! # Testing
281//!
282//! This crate is validated through a combination of:
283//!
284//! * Unit tests
285//! * Property-based tests using a naive map as an oracle
286//! * Chaos tests for several kinds of buggy `Eq` and `Ord` implementations
287//! * Miri tests for unsafe code
288//!
289//! If you see a gap in testing, new tests are welcome. Thank you!
290//!
291//! # No-std compatibility
292//!
293//! Most of this crate is no-std compatible, though [`alloc`] is required.
294//!
295//! The [`IdOrdMap`] type is not currently no-std compatible due to its use of a
296//! thread-local. This thread-local is just a way to work around a limitation in
297//! std's `BTreeMap` API, though. Either a custom B-Tree implementation, or a
298//! platform-specific notion of thread locals, would suffice to make
299//! [`IdOrdMap`] no-std compatible.
300//!
301//! # Optional features
302//!
303//! - `allocator-api2`: Enables support for custom allocators via the
304//!   [`allocator_api2`] crate. Both global and scoped/arena allocators
305//!   (such as `bumpalo`) are supported. Custom allocators are not currently
306//!   supported by `IdOrdMap`.
307//! - `daft`: Enables [`daft`] support for all ID map types. *Not enabled by
308//!   default.*
309//! - `default-hasher`: Enables the `DefaultHashBuilder` type. Disable this
310//!   feature to require a hash builder type parameter to be passed into
311//!   [`IdHashMap`], [`BiHashMap`], and [`TriHashMap`]. *Enabled by default.*
312//! - `proptest`: Enables [`proptest`] support for all ID map types, providing
313//!   [`Arbitrary`] implementations and strategies for property-based testing.
314//!   *Not enabled by default.*
315//! - `schemars08`: Enables [`schemars`] support for all ID map types,
316//!   including support for [automatic replacement] through [`typify`] or
317//!   [`dropshot`]. *Not enabled by default.*
318//! - `serde`: Enables serde support for all ID map types. *Not enabled by
319//!   default.*
320//! - `std`: Enables std support. *Enabled by default.*
321//!
322//! # Related work
323//!
324//! - [`bimap`](https://docs.rs/bimap) provides a bijective map, but does not
325//!   have a way to associate arbitrary values with each pair of keys. However,
326//!   it does support an ordered map type without the need for std.
327//!
328//! - [`multi_index_map`](https://crates.io/crates/multi_index_map) provides
329//!   maps with arbitrary indexes on fields, and is more flexible than this
330//!   crate. However, it doesn't expose generic traits for map types, and it
331//!   requires key types to be `Clone`. In `iddqd`, we pick a somewhat different
332//!   point in the design space, but we think `multi_index_map` is also great.
333//!
334//! - In general, this is similar to relational database records with
335//!   indexes. For sufficiently complex use cases, consider an embedded
336//!   database like [SQLite](https://www.sqlite.org/), or even a networked
337//!   database like [PostgreSQL](https://www.postgresql.org/). `iddqd` is a
338//!   good fit for simple in-memory caches of data stored in these databases.
339//!
340//! # Minimum supported Rust version (MSRV)
341//!
342//! This crate's MSRV is **Rust 1.81**. In general we aim for 6 months of Rust
343//! compatibility.
344//!
345//! # What does iddqd mean?
346//!
347//! The name `iddqd` is a reference to [a cheat
348//! code](https://doomwiki.org/wiki/Doom_cheat_codes) in the classic video game
349//! _Doom_. It has `id` in the name, and is short and memorable.
350//!
351//! [`Borrow`]: core::borrow::Borrow
352//! [JSON Schema]: https://json-schema.org/
353//! [OpenAPI]: https://www.openapis.org/
354//! [`schemars`]: https://crates.io/crates/schemars
355//! [automatic replacement]: https://github.com/oxidecomputer/iddqd/blob/main/crates/iddqd-extended-examples/examples/typify-types.rs
356//! [`typify`]: https://crates.io/crates/typify
357//! [`dropshot`]: https://crates.io/crates/dropshot
358//! [`Arbitrary`]: proptest::arbitrary::Arbitrary
359
360#![no_std]
361#![cfg_attr(doc_cfg, feature(doc_auto_cfg))]
362#![warn(missing_docs)]
363
364#[cfg_attr(not(feature = "std"), macro_use)] // for `format!`
365extern crate alloc;
366#[cfg(feature = "std")]
367#[macro_use]
368extern crate std;
369
370// This must go first so macros can be used in the rest of the crate.
371#[macro_use]
372mod macros;
373
374pub mod bi_hash_map;
375pub mod errors;
376pub mod id_hash_map;
377#[cfg(feature = "std")]
378pub mod id_ord_map;
379#[doc(hidden)]
380pub mod internal;
381mod support;
382pub mod tri_hash_map;
383
384pub use bi_hash_map::{imp::BiHashMap, trait_defs::BiHashItem};
385// Re-exports of equivalent traits. Comparable is only used by IdOrdMap, hence
386// is restricted to std.
387#[cfg(feature = "std")]
388#[doc(no_inline)]
389pub use equivalent::Comparable;
390#[doc(no_inline)]
391pub use equivalent::Equivalent;
392pub use id_hash_map::{imp::IdHashMap, trait_defs::IdHashItem};
393#[cfg(feature = "std")]
394pub use id_ord_map::{imp::IdOrdMap, trait_defs::IdOrdItem};
395#[cfg(feature = "daft")]
396pub use support::daft_utils::IdLeaf;
397pub use support::hash_builder::DefaultHashBuilder;
398pub use tri_hash_map::{imp::TriHashMap, trait_defs::TriHashItem};