Skip to main content

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 query type `Q`:
204//!
205//! * The hash map types require `Q: Hash + Equivalent<T::Key<'_>>`. Also, `Q`
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 `Q: Comparable<T::Key<'_>>`, which in turn requires
210//!   `Equivalent<T::Key<'_>>`. (There's no need for `Q` 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//! ### Mutable lookups take owned keys
281//!
282//! All `&mut self` methods take owned instances of the key type directly rather
283//! than references. Unlike the standard library's maps, the key type itself can
284//! be borrowed, so this generally does not require giving up on efficiency.
285//!
286//! For example, consider a standard library `HashMap<String, u32>`. Its
287//! `remove` method takes `&Q` where `String: Borrow<Q>`, which is what lets
288//! you pass a `&str`. Without `Borrow`, `remove` would have to take a
289//! `&String`, and removing by name would mean allocating a `String` first:
290//!
291//! ```
292//! use std::collections::HashMap;
293//!
294//! let mut map: HashMap<String, u32> = HashMap::new();
295//! map.insert("foo".to_owned(), 1);
296//!
297//! // Without Borrow, this is the only option: allocate a String to remove
298//! // by name.
299//! let owned_key = "foo".to_owned();
300//! assert_eq!(map.remove(&owned_key), Some(1));
301//!
302//! map.insert("foo".to_owned(), 1);
303//!
304//! // With Borrow, a &str works directly.
305//! assert_eq!(map.remove("foo"), Some(1));
306//! ```
307//!
308//! With [`IdHashMap`], the key type can be a borrowed form such as `&'a str`.
309//! Passing the key by value then means passing a `&str`, so no allocation is
310//! needed:
311//!
312//! ```
313//! # #[cfg(feature = "default-hasher")] {
314//! use iddqd::{IdHashItem, IdHashMap, id_upcast};
315//!
316//! #[derive(Debug, PartialEq)]
317//! struct Counter {
318//!     name: String,
319//!     value: u32,
320//! }
321//!
322//! impl IdHashItem for Counter {
323//!     type Key<'a> = &'a str;
324//!
325//!     fn key(&self) -> Self::Key<'_> {
326//!         &self.name
327//!     }
328//!
329//!     id_upcast!();
330//! }
331//!
332//! let mut map = IdHashMap::new();
333//! map.insert_unique(Counter { name: "foo".to_owned(), value: 1 }).unwrap();
334//!
335//! // The key type is &str, so passing it by value is passing a borrow.
336//! assert_eq!(
337//!     map.remove("foo"),
338//!     Some(Counter { name: "foo".to_owned(), value: 1 }),
339//! );
340//! # }
341//! ```
342//!
343//! This is a soundness requirement.
344//!
345//! # Testing and verification
346//!
347//! This crate is validated through a combination of:
348//!
349//! * Unit tests
350//! * Property-based tests using a naive map as an oracle
351//! * Unit and property-based tests for several kinds of pathological `Eq`,
352//!   `Hash` and `Ord` implementations
353//! * Property-based tests for panic safety
354//! * Miri tests for unsafe code, checked against both Stacked and Tree Borrows
355//! * Compile-fail UI tests to ensure invalid patterns are rejected by the
356//!   borrow checker
357//! * Bounded formal verification for core invariants and UB using
358//!   [Soteria](https://soteria-tools.com)
359//!
360//! Pathological user implementations written in safe Rust might corrupt
361//! internal map state and result in panics, but should never cause UB.
362//!
363//! For more information on our validation philosophy, see [this Oxide blog
364//! entry](https://oxide.computer/blog/iddqd-unsafe).
365//!
366//! If you see a gap in testing, new tests are welcome. Thank you!
367//!
368//! Note that **testing requires [cargo-nextest](https://nexte.st/)**: several
369//! tests depend on nextest's process-per-test model.
370//!
371//! # No-std compatibility
372//!
373//! Most of this crate is no-std compatible, though [`alloc`] is required.
374//!
375//! The [`IdOrdMap`] type is not currently no-std compatible due to its use of a
376//! thread-local. This thread-local is just a way to work around a limitation in
377//! std's `BTreeMap` API, though. Either a custom B-Tree implementation, or a
378//! platform-specific notion of thread locals, would suffice to make
379//! [`IdOrdMap`] no-std compatible.
380//!
381//! # Optional features
382//!
383//! - `allocator-api2`: Enables support for custom allocators via the
384//!   [`allocator_api2`] crate. Both global and scoped/arena allocators
385//!   (such as `bumpalo`) are supported. Custom allocators are not currently
386//!   supported by `IdOrdMap`.
387//! - `daft`: Enables [`daft`] support for all ID map types. *Not enabled by
388//!   default.*
389//! - `default-hasher`: Enables the `DefaultHashBuilder` type. Disable this
390//!   feature to require a hash builder type parameter to be passed into
391//!   [`IdHashMap`], [`BiHashMap`], and [`TriHashMap`]. *Enabled by default.*
392//! - `proptest`: Enables [`proptest`] support for all ID map types, providing
393//!   [`Arbitrary`] implementations and strategies for property-based testing.
394//!   *Not enabled by default.*
395//! - `schemars08`: Enables [`schemars`] support for all ID map types,
396//!   including support for [automatic replacement] through [`typify`] or
397//!   [`dropshot`]. *Not enabled by default.*
398//! - `serde`: Enables serde support for all ID map types. *Not enabled by
399//!   default.*
400//! - `std`: Enables std support. *Enabled by default.*
401//!
402//! # Related work
403//!
404//! - [`bimap`](https://docs.rs/bimap) provides a bijective map, but does not
405//!   have a way to associate arbitrary values with each pair of keys. However,
406//!   it does support an ordered map type without the need for std.
407//!
408//! - [`multi_index_map`](https://crates.io/crates/multi_index_map) provides
409//!   maps with arbitrary indexes on fields, and is more flexible than this
410//!   crate. However, it doesn't expose generic traits for map types, and it
411//!   requires key types to be `Clone`. In `iddqd`, we pick a somewhat different
412//!   point in the design space, but we think `multi_index_map` is also great.
413//!
414//! - In general, this is similar to relational database records with
415//!   indexes. For sufficiently complex use cases, consider an embedded
416//!   database like [SQLite](https://www.sqlite.org/), or even a networked
417//!   database like [PostgreSQL](https://www.postgresql.org/). `iddqd` is a
418//!   good fit for simple in-memory caches of data stored in these databases.
419//!
420//! # Minimum supported Rust version (MSRV)
421//!
422//! This crate's MSRV is **Rust 1.86**. In general we aim for 6 months of Rust
423//! compatibility.
424//!
425//! # What does iddqd mean?
426//!
427//! The name `iddqd` is a reference to [a cheat
428//! code](https://doomwiki.org/wiki/Doom_cheat_codes) in the classic video game
429//! _Doom_. It has `id` in the name, and is short and memorable.
430//!
431//! [`Borrow`]: core::borrow::Borrow
432//! [JSON Schema]: https://json-schema.org/
433//! [OpenAPI]: https://www.openapis.org/
434//! [`schemars`]: https://crates.io/crates/schemars
435//! [automatic replacement]: https://github.com/oxidecomputer/iddqd/blob/main/crates/iddqd-extended-examples/examples/typify-types.rs
436//! [`typify`]: https://crates.io/crates/typify
437//! [`dropshot`]: https://crates.io/crates/dropshot
438//! [`Arbitrary`]: proptest::arbitrary::Arbitrary
439
440#![no_std]
441#![cfg_attr(doc_cfg, feature(doc_cfg))]
442#![warn(missing_docs)]
443
444#[cfg_attr(not(feature = "std"), macro_use)] // for `format!`
445extern crate alloc;
446#[cfg(feature = "std")]
447#[macro_use]
448extern crate std;
449
450// This must go first so macros can be used in the rest of the crate.
451#[macro_use]
452mod macros;
453
454pub mod bi_hash_map;
455pub mod errors;
456pub mod id_hash_map;
457#[cfg(feature = "std")]
458pub mod id_ord_map;
459#[doc(hidden)]
460pub mod internal;
461#[cfg(soteria)]
462mod proofs;
463mod support;
464pub mod tri_hash_map;
465
466pub use bi_hash_map::{imp::BiHashMap, trait_defs::BiHashItem};
467// Re-exports of equivalent traits. Comparable is only used by IdOrdMap, hence
468// is restricted to std.
469#[cfg(feature = "std")]
470#[doc(no_inline)]
471pub use equivalent::Comparable;
472#[doc(no_inline)]
473pub use equivalent::Equivalent;
474pub use id_hash_map::{imp::IdHashMap, trait_defs::IdHashItem};
475#[cfg(feature = "std")]
476pub use id_ord_map::{imp::IdOrdMap, trait_defs::IdOrdItem};
477#[cfg(feature = "daft")]
478pub use support::daft_utils::IdLeaf;
479pub use support::hash_builder::DefaultHashBuilder;
480pub use tri_hash_map::{imp::TriHashMap, trait_defs::TriHashItem};