Skip to content

Commit

Permalink
RUST-2027 Impl Hash/Eq for BSON (#495)
Browse files Browse the repository at this point in the history
  • Loading branch information
NineLord authored Sep 6, 2024
1 parent 20c56f0 commit 28e3925
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 4 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ chrono-0_4 = ["chrono"]
uuid-1 = []
# if enabled, include API for interfacing with time 0.3
time-0_3 = []
# If enabled, implement Hash/Eq for Bson and Document
hashable = []
serde_path_to_error = ["dep:serde_path_to_error"]
# if enabled, include serde_with interop.
# should be used in conjunction with chrono-0_4 or uuid-0_8.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Note that if you are using `bson` through the `mongodb` crate, you do not need t
| `serde_with` | Enable [`serde_with`](https://docs.rs/serde_with/1.x) 1.x integrations for `bson::DateTime` and `bson::Uuid`.| serde_with | no |
| `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 |
| `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 |
| `hashable` | Implement `core::hash::Hash` and `std::cmp::Eq` on `Bson` and `Document`. | n/a | no |

## Overview of the BSON Format

Expand Down
2 changes: 1 addition & 1 deletion src/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
};

/// Represents a BSON binary value.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Binary {
/// The subtype of the bytes.
pub subtype: BinarySubtype,
Expand Down
44 changes: 42 additions & 2 deletions src/bson.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use std::{
convert::{TryFrom, TryInto},
fmt::{self, Debug, Display, Formatter},
hash::Hash,
};

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

#[cfg(feature = "hashable")]
impl Hash for Bson {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
Bson::Double(double) => {
if *double == 0.0_f64 {
// There are 2 zero representations, +0 and -0, which
// compare equal but have different bits. We use the +0 hash
// for both so that hash(+0) == hash(-0).
0.0_f64.to_bits().hash(state);
} else {
double.to_bits().hash(state);
}
}
Bson::String(x) => x.hash(state),
Bson::Array(x) => x.hash(state),
Bson::Document(x) => x.hash(state),
Bson::Boolean(x) => x.hash(state),
Bson::RegularExpression(x) => x.hash(state),
Bson::JavaScriptCode(x) => x.hash(state),
Bson::JavaScriptCodeWithScope(x) => x.hash(state),
Bson::Int32(x) => x.hash(state),
Bson::Int64(x) => x.hash(state),
Bson::Timestamp(x) => x.hash(state),
Bson::Binary(x) => x.hash(state),
Bson::ObjectId(x) => x.hash(state),
Bson::DateTime(x) => x.hash(state),
Bson::Symbol(x) => x.hash(state),
Bson::Decimal128(x) => x.hash(state),
Bson::DbPointer(x) => x.hash(state),
Bson::Null | Bson::Undefined | Bson::MaxKey | Bson::MinKey => (),
}
}
}

#[cfg(feature = "hashable")]
impl Eq for Bson {}

impl Display for Bson {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match *self {
Expand Down Expand Up @@ -1046,7 +1085,7 @@ impl Timestamp {
}

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

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

/// Represents a DBPointer. (Deprecated)
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct DbPointer {
pub(crate) namespace: String,
pub(crate) id: oid::ObjectId,
Expand Down
2 changes: 1 addition & 1 deletion src/decimal128.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use bitvec::prelude::*;
/// # }
/// # example().unwrap()
/// ```
#[derive(Copy, Clone, PartialEq)]
#[derive(Copy, Clone, Hash, PartialEq, Eq)]
pub struct Decimal128 {
/// BSON bytes containing the decimal128. Stored for round tripping.
pub(crate) bytes: [u8; 16],
Expand Down
12 changes: 12 additions & 0 deletions src/document.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! A BSON document represented as an associative HashMap with insertion ordering.
#[cfg(feature = "hashable")]
use std::hash::Hash;
use std::{
error,
fmt::{self, Debug, Display, Formatter},
Expand Down Expand Up @@ -56,6 +58,7 @@ impl error::Error for ValueAccessError {}

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

#[cfg(feature = "hashable")]
impl Hash for Document {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let mut entries = Vec::from_iter(&self.inner);
entries.sort_unstable_by(|a, b| a.0.cmp(b.0));
entries.hash(state);
}
}

impl Display for Document {
fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
fmt.write_str("{")?;
Expand Down
16 changes: 16 additions & 0 deletions src/tests/modules/bson.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,19 @@ fn debug_print() {
assert_eq!(format!("{:?}", doc), normal_print);
assert_eq!(format!("{:#?}", doc), pretty_print);
}

#[cfg(feature = "hashable")]
#[test]
fn test_hashable() {
let mut map = std::collections::HashMap::new();
map.insert(bson!({"a":1, "b": 2}), 1);
map.insert(Bson::Null, 2);
map.insert(Bson::Undefined, 3);

let key = bson!({"b": 2, "a":1});
assert_eq!(map.remove(&key), Some(1));
assert_eq!(map.remove(&Bson::Undefined), Some(3));
assert_eq!(map.remove(&Bson::Null), Some(2));

assert!(map.is_empty());
}

0 comments on commit 28e3925

Please sign in to comment.