Skip to main content

object_hash/
lib.rs

1#![doc = include_str!("../README.md")]
2mod encode;
3mod sha1;
4
5use std::fmt;
6
7/// A value that can be hashed.
8///
9/// This is the set of things the reference can hash, minus the ones that only
10/// exist in JavaScript: there is no case for a function, a symbol, or a `Date`,
11/// because a Rust caller would not pass one. A `Map` here stands for the
12/// reference's plain object and its `Map`, which it treats the same way.
13#[derive(Debug, Clone, PartialEq)]
14pub enum Value {
15    /// The absence of a value. Written `Null`, not the empty string.
16    Null,
17    /// A boolean.
18    Bool(bool),
19    /// A number. An integer is written as a float that happens to be integral,
20    /// because the reference has no separate integer case.
21    Number(f64),
22    /// A string.
23    String(String),
24    /// A sequence whose order matters.
25    Array(Vec<Value>),
26    /// A collection whose order does not matter.
27    Set(Vec<Value>),
28    /// A mapping from strings to values, written in key order.
29    Map(Vec<(String, Value)>),
30}
31
32impl Value {
33    /// A string value from anything string-like.
34    #[must_use]
35    pub fn string(text: impl Into<String>) -> Self {
36        Self::String(text.into())
37    }
38
39    /// A number value.
40    #[must_use]
41    pub fn number(number: impl Into<f64>) -> Self {
42        Self::Number(number.into())
43    }
44
45    /// Whether this value is a scalar.
46    ///
47    /// Scalars are the values a JavaScript `Set` deduplicates; arrays and maps
48    /// it compares by identity instead. Nothing outside the crate needs this,
49    /// but it is the distinction the encoding turns on.
50    pub(crate) fn is_scalar(&self) -> bool {
51        matches!(
52            self,
53            Self::Null | Self::Bool(_) | Self::Number(_) | Self::String(_)
54        )
55    }
56
57    /// The digest of this value, as lowercase hexadecimal SHA-1.
58    #[must_use]
59    pub fn hash(&self) -> String {
60        hash(self)
61    }
62}
63
64impl fmt::Display for Value {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.write_str(&canonical(self))
67    }
68}
69
70impl From<bool> for Value {
71    fn from(value: bool) -> Self {
72        Self::Bool(value)
73    }
74}
75
76impl From<f64> for Value {
77    fn from(value: f64) -> Self {
78        Self::Number(value)
79    }
80}
81
82impl From<i64> for Value {
83    fn from(value: i64) -> Self {
84        Self::Number(value as f64)
85    }
86}
87
88impl From<i32> for Value {
89    fn from(value: i32) -> Self {
90        Self::Number(f64::from(value))
91    }
92}
93
94impl From<u32> for Value {
95    fn from(value: u32) -> Self {
96        Self::Number(f64::from(value))
97    }
98}
99
100impl From<&str> for Value {
101    fn from(value: &str) -> Self {
102        Self::String(value.to_string())
103    }
104}
105
106impl From<String> for Value {
107    fn from(value: String) -> Self {
108        Self::String(value)
109    }
110}
111
112impl<T: Into<Value>> From<Vec<T>> for Value {
113    fn from(value: Vec<T>) -> Self {
114        Self::Array(value.into_iter().map(Into::into).collect())
115    }
116}
117
118/// The canonical form of a value, as text.
119///
120/// Two values that are equal have the same canonical form. It is returned rather
121/// than hashed so a caller can use a different digest, or none.
122#[must_use]
123pub fn canonical(value: &Value) -> String {
124    encode::canonical(value)
125}
126
127/// The digest of a value, as lowercase hexadecimal SHA-1.
128///
129/// The digest is stable across runs and machines for equal values. See the
130/// crate documentation for what equal means here.
131///
132/// # Example
133///
134/// ```
135/// use object_hash::{hash, Value};
136///
137/// let left = Value::Map(vec![
138///     ("b".into(), Value::number(2)),
139///     ("a".into(), Value::string("one")),
140/// ]);
141/// // A map's key order does not change the digest.
142/// let right = Value::Map(vec![
143///     ("a".into(), Value::string("one")),
144///     ("b".into(), Value::number(2)),
145/// ]);
146/// assert_eq!(hash(&left), hash(&right));
147///
148/// // An array's order does.
149/// assert_ne!(
150///     hash(&Value::Array(vec![Value::number(1), Value::number(2)])),
151///     hash(&Value::Array(vec![Value::number(2), Value::number(1)])),
152/// );
153/// ```
154#[must_use]
155pub fn hash(value: &Value) -> String {
156    sha1::hex(canonical(value).as_bytes())
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn a_digest_is_forty_hexadecimal_characters() {
165        let digest = hash(&Value::Null);
166        assert_eq!(digest.len(), 40);
167        assert!(digest.chars().all(|c| c.is_ascii_hexdigit()));
168    }
169
170    #[test]
171    fn equal_values_hash_the_same_and_different_values_do_not() {
172        assert_eq!(hash(&Value::Number(1.0)), hash(&Value::Number(1.0)));
173        assert_ne!(hash(&Value::Number(1.0)), hash(&Value::Number(2.0)));
174        assert_ne!(hash(&Value::Number(1.0)), hash(&Value::String("1".into())));
175        assert_ne!(hash(&Value::Null), hash(&Value::String("Null".into())));
176    }
177
178    #[test]
179    fn the_conversions_build_the_expected_values() {
180        assert_eq!(Value::from("a"), Value::String("a".into()));
181        assert_eq!(Value::from(1_i32), Value::Number(1.0));
182        assert_eq!(Value::from(true), Value::Bool(true));
183        assert_eq!(
184            Value::from(vec![1_i32, 2]),
185            Value::Array(vec![Value::Number(1.0), Value::Number(2.0)])
186        );
187    }
188
189    #[test]
190    fn the_display_form_is_the_canonical_form() {
191        assert_eq!(Value::Bool(true).to_string(), "bool:true");
192        assert_eq!(Value::Null.to_string(), "Null");
193    }
194}