Skip to content

Commit 28e3925

Browse files
authored
RUST-2027 Impl Hash/Eq for BSON (#495)
1 parent 20c56f0 commit 28e3925

File tree

7 files changed

+75
-4
lines changed

7 files changed

+75
-4
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ chrono-0_4 = ["chrono"]
4343
uuid-1 = []
4444
# if enabled, include API for interfacing with time 0.3
4545
time-0_3 = []
46+
# If enabled, implement Hash/Eq for Bson and Document
47+
hashable = []
4648
serde_path_to_error = ["dep:serde_path_to_error"]
4749
# if enabled, include serde_with interop.
4850
# should be used in conjunction with chrono-0_4 or uuid-0_8.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Note that if you are using `bson` through the `mongodb` crate, you do not need t
5454
| `serde_with` | Enable [`serde_with`](https://docs.rs/serde_with/1.x) 1.x integrations for `bson::DateTime` and `bson::Uuid`.| serde_with | no |
5555
| `serde_with-3` | Enable [`serde_with`](https://docs.rs/serde_with/3.x) 3.x integrations for `bson::DateTime` and `bson::Uuid`.| serde_with | no |
5656
| `serde_path_to_error` | Enable support for error paths via integration with [`serde_path_to_error`](https://docs.rs/serde_path_to_err/latest). This is an unstable feature and any breaking changes to `serde_path_to_error` may affect usage of it via this feature. | serde_path_to_error | no |
57+
| `hashable` | Implement `core::hash::Hash` and `std::cmp::Eq` on `Bson` and `Document`. | n/a | no |
5758

5859
## Overview of the BSON Format
5960

src/binary.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
};
77

88
/// Represents a BSON binary value.
9-
#[derive(Debug, Clone, PartialEq)]
9+
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
1010
pub struct Binary {
1111
/// The subtype of the bytes.
1212
pub subtype: BinarySubtype,

src/bson.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use std::{
2525
convert::{TryFrom, TryInto},
2626
fmt::{self, Debug, Display, Formatter},
27+
hash::Hash,
2728
};
2829

2930
use serde_json::{json, Value};
@@ -87,6 +88,44 @@ pub enum Bson {
8788
/// Alias for `Vec<Bson>`.
8889
pub type Array = Vec<Bson>;
8990

91+
#[cfg(feature = "hashable")]
92+
impl Hash for Bson {
93+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
94+
match self {
95+
Bson::Double(double) => {
96+
if *double == 0.0_f64 {
97+
// There are 2 zero representations, +0 and -0, which
98+
// compare equal but have different bits. We use the +0 hash
99+
// for both so that hash(+0) == hash(-0).
100+
0.0_f64.to_bits().hash(state);
101+
} else {
102+
double.to_bits().hash(state);
103+
}
104+
}
105+
Bson::String(x) => x.hash(state),
106+
Bson::Array(x) => x.hash(state),
107+
Bson::Document(x) => x.hash(state),
108+
Bson::Boolean(x) => x.hash(state),
109+
Bson::RegularExpression(x) => x.hash(state),
110+
Bson::JavaScriptCode(x) => x.hash(state),
111+
Bson::JavaScriptCodeWithScope(x) => x.hash(state),
112+
Bson::Int32(x) => x.hash(state),
113+
Bson::Int64(x) => x.hash(state),
114+
Bson::Timestamp(x) => x.hash(state),
115+
Bson::Binary(x) => x.hash(state),
116+
Bson::ObjectId(x) => x.hash(state),
117+
Bson::DateTime(x) => x.hash(state),
118+
Bson::Symbol(x) => x.hash(state),
119+
Bson::Decimal128(x) => x.hash(state),
120+
Bson::DbPointer(x) => x.hash(state),
121+
Bson::Null | Bson::Undefined | Bson::MaxKey | Bson::MinKey => (),
122+
}
123+
}
124+
}
125+
126+
#[cfg(feature = "hashable")]
127+
impl Eq for Bson {}
128+
90129
impl Display for Bson {
91130
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
92131
match *self {
@@ -1046,7 +1085,7 @@ impl Timestamp {
10461085
}
10471086

10481087
/// Represents a BSON regular expression value.
1049-
#[derive(Debug, Clone, PartialEq)]
1088+
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
10501089
pub struct Regex {
10511090
/// The regex pattern to match.
10521091
pub pattern: String,
@@ -1081,6 +1120,7 @@ impl Display for Regex {
10811120

10821121
/// Represents a BSON code with scope value.
10831122
#[derive(Debug, Clone, PartialEq)]
1123+
#[cfg_attr(feature = "hashable", derive(Eq, Hash))]
10841124
pub struct JavaScriptCodeWithScope {
10851125
/// The JavaScript code.
10861126
pub code: String,
@@ -1096,7 +1136,7 @@ impl Display for JavaScriptCodeWithScope {
10961136
}
10971137

10981138
/// Represents a DBPointer. (Deprecated)
1099-
#[derive(Debug, Clone, PartialEq)]
1139+
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
11001140
pub struct DbPointer {
11011141
pub(crate) namespace: String,
11021142
pub(crate) id: oid::ObjectId,

src/decimal128.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use bitvec::prelude::*;
2121
/// # }
2222
/// # example().unwrap()
2323
/// ```
24-
#[derive(Copy, Clone, PartialEq)]
24+
#[derive(Copy, Clone, Hash, PartialEq, Eq)]
2525
pub struct Decimal128 {
2626
/// BSON bytes containing the decimal128. Stored for round tripping.
2727
pub(crate) bytes: [u8; 16],

src/document.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! A BSON document represented as an associative HashMap with insertion ordering.
22
3+
#[cfg(feature = "hashable")]
4+
use std::hash::Hash;
35
use std::{
46
error,
57
fmt::{self, Debug, Display, Formatter},
@@ -56,6 +58,7 @@ impl error::Error for ValueAccessError {}
5658

5759
/// A BSON document represented as an associative HashMap with insertion ordering.
5860
#[derive(Clone, PartialEq)]
61+
#[cfg_attr(feature = "hashable", derive(Eq))]
5962
pub struct Document {
6063
inner: IndexMap<String, Bson, RandomState>,
6164
}
@@ -66,6 +69,15 @@ impl Default for Document {
6669
}
6770
}
6871

72+
#[cfg(feature = "hashable")]
73+
impl Hash for Document {
74+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
75+
let mut entries = Vec::from_iter(&self.inner);
76+
entries.sort_unstable_by(|a, b| a.0.cmp(b.0));
77+
entries.hash(state);
78+
}
79+
}
80+
6981
impl Display for Document {
7082
fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
7183
fmt.write_str("{")?;

src/tests/modules/bson.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,3 +486,19 @@ fn debug_print() {
486486
assert_eq!(format!("{:?}", doc), normal_print);
487487
assert_eq!(format!("{:#?}", doc), pretty_print);
488488
}
489+
490+
#[cfg(feature = "hashable")]
491+
#[test]
492+
fn test_hashable() {
493+
let mut map = std::collections::HashMap::new();
494+
map.insert(bson!({"a":1, "b": 2}), 1);
495+
map.insert(Bson::Null, 2);
496+
map.insert(Bson::Undefined, 3);
497+
498+
let key = bson!({"b": 2, "a":1});
499+
assert_eq!(map.remove(&key), Some(1));
500+
assert_eq!(map.remove(&Bson::Undefined), Some(3));
501+
assert_eq!(map.remove(&Bson::Null), Some(2));
502+
503+
assert!(map.is_empty());
504+
}

0 commit comments

Comments
 (0)