nimbus_fml/defaults/
hasher.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::schema::TypeQuery;
6use crate::{
7    intermediate_representation::{FeatureDef, ObjectDef, PropDef, TypeRef},
8    schema::Sha256Hasher,
9};
10use serde_json::Value;
11use std::{
12    collections::{BTreeMap, BTreeSet, HashSet},
13    hash::{Hash, Hasher},
14};
15
16pub(crate) struct DefaultsHasher<'a> {
17    object_defs: &'a BTreeMap<String, ObjectDef>,
18}
19
20impl<'a> DefaultsHasher<'a> {
21    pub(crate) fn new(objs: &'a BTreeMap<String, ObjectDef>) -> Self {
22        Self { object_defs: objs }
23    }
24
25    pub(crate) fn hash(&self, feature_def: &FeatureDef) -> u64 {
26        let mut hasher = Sha256Hasher::default();
27        feature_def.defaults_hash(&mut hasher);
28
29        let types = self.all_types(feature_def);
30
31        // We iterate through the object_defs because they are both
32        // ordered, and we want to maintain a stable ordering.
33        // By contrast, `types`, a HashSet, definitely does not have a stable ordering.
34        for (name, obj_def) in self.object_defs {
35            if types.contains(&TypeRef::Object(name.clone())) {
36                obj_def.defaults_hash(&mut hasher);
37            }
38        }
39
40        hasher.finish()
41    }
42
43    fn all_types(&self, feature_def: &FeatureDef) -> HashSet<TypeRef> {
44        TypeQuery::new(self.object_defs).all_types(feature_def)
45    }
46}
47
48trait DefaultsHash {
49    fn defaults_hash<H: Hasher>(&self, state: &mut H);
50}
51
52impl DefaultsHash for FeatureDef {
53    fn defaults_hash<H: Hasher>(&self, state: &mut H) {
54        self.props.defaults_hash(state);
55    }
56}
57
58impl DefaultsHash for Vec<PropDef> {
59    fn defaults_hash<H: Hasher>(&self, state: &mut H) {
60        let mut vec = self.iter().collect::<Vec<_>>();
61        vec.sort_by_key(|item| &item.name);
62
63        for item in vec {
64            item.defaults_hash(state);
65        }
66    }
67}
68
69impl DefaultsHash for PropDef {
70    fn defaults_hash<H: Hasher>(&self, state: &mut H) {
71        self.name.hash(state);
72        self.default.defaults_hash(state);
73    }
74}
75
76impl DefaultsHash for ObjectDef {
77    fn defaults_hash<H: Hasher>(&self, state: &mut H) {
78        self.props.defaults_hash(state);
79    }
80}
81
82impl DefaultsHash for Value {
83    fn defaults_hash<H: Hasher>(&self, state: &mut H) {
84        match self {
85            Self::Null => 0_u8.hash(state),
86            Self::Number(v) => v.hash(state),
87            Self::Bool(v) => v.hash(state),
88            Self::String(v) => v.hash(state),
89            Self::Array(array) => {
90                for v in array {
91                    v.defaults_hash(state);
92                }
93            }
94            Self::Object(map) => {
95                let keys = map.keys().collect::<BTreeSet<_>>();
96                for k in keys {
97                    let v = map.get(k).unwrap();
98                    v.defaults_hash(state);
99                }
100            }
101        }
102    }
103}
104
105#[cfg(test)]
106mod unit_tests {
107    use super::*;
108    use crate::error::Result;
109
110    use serde_json::json;
111
112    #[test]
113    fn test_simple_feature_stable_over_time() -> Result<()> {
114        let objs = Default::default();
115
116        let feature_def = {
117            let p1 = PropDef::new("my-int", &TypeRef::Int, &json!(1));
118            let p2 = PropDef::new("my-bool", &TypeRef::Boolean, &json!(true));
119            let p3 = PropDef::new("my-string", &TypeRef::String, &json!("string"));
120            FeatureDef::new("test_feature", "", vec![p1, p2, p3], false)
121        };
122
123        let mut prev: Option<u64> = None;
124        for _ in 0..100 {
125            let hasher = DefaultsHasher::new(&objs);
126            let hash = hasher.hash(&feature_def);
127            if let Some(prev) = prev {
128                assert_eq!(prev, hash);
129            }
130            prev = Some(hash);
131        }
132
133        Ok(())
134    }
135
136    #[test]
137    fn test_simple_feature_is_stable_with_props_in_any_order() -> Result<()> {
138        let objs = Default::default();
139
140        let p1 = PropDef::new("my-int", &TypeRef::Int, &json!(1));
141        let p2 = PropDef::new("my-bool", &TypeRef::Boolean, &json!(true));
142        let p3 = PropDef::new("my-string", &TypeRef::String, &json!("string"));
143
144        let f1 = FeatureDef::new(
145            "test_feature",
146            "",
147            vec![p1.clone(), p2.clone(), p3.clone()],
148            false,
149        );
150        let f2 = FeatureDef::new("test_feature", "", vec![p3, p2, p1], false);
151
152        let hasher = DefaultsHasher::new(&objs);
153        assert_eq!(hasher.hash(&f1), hasher.hash(&f2));
154        Ok(())
155    }
156
157    #[test]
158    fn test_simple_feature_is_stable_changing_types() -> Result<()> {
159        let objs = Default::default();
160
161        // unsure how you'd do this.
162        let f1 = {
163            let prop1 = PropDef::new("p1", &TypeRef::Int, &json!(42));
164            let prop2 = PropDef::new("p2", &TypeRef::String, &json!("Yes"));
165            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
166        };
167
168        let f2 = {
169            let prop1 = PropDef::new("p1", &TypeRef::String, &json!(42));
170            let prop2 = PropDef::new("p2", &TypeRef::Int, &json!("Yes"));
171            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
172        };
173
174        let hasher = DefaultsHasher::new(&objs);
175        assert_eq!(hasher.hash(&f1), hasher.hash(&f2));
176
177        Ok(())
178    }
179
180    #[test]
181    fn test_simple_feature_is_sensitive_to_change() -> Result<()> {
182        let objs = Default::default();
183
184        let f1 = {
185            let prop1 = PropDef::new("p1", &TypeRef::String, &json!("Yes"));
186            let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(1));
187            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
188        };
189
190        let hasher = DefaultsHasher::new(&objs);
191
192        // Sensitive to change in type of properties
193        let ne = {
194            let prop1 = PropDef::new("p1", &TypeRef::String, &json!("Nope"));
195            let prop2 = PropDef::new("p2", &TypeRef::Boolean, &json!(1));
196            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
197        };
198        assert_ne!(hasher.hash(&f1), hasher.hash(&ne));
199
200        // Sensitive to change in name of properties
201        let ne = {
202            let prop1 = PropDef::new("p1_", &TypeRef::String, &json!("Yes"));
203            let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(1));
204            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
205        };
206        assert_ne!(hasher.hash(&f1), hasher.hash(&ne));
207
208        // Not Sensitive to change in changes in coenrollment status
209        let eq = {
210            let prop1 = PropDef::new("p1", &TypeRef::String, &json!("Yes"));
211            let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(1));
212            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], true)
213        };
214        assert_eq!(hasher.hash(&f1), hasher.hash(&eq));
215
216        Ok(())
217    }
218
219    #[test]
220    fn test_feature_is_sensitive_to_object_change() -> Result<()> {
221        let obj_nm = "MyObject";
222        let obj_t = TypeRef::Object(obj_nm.to_string());
223
224        let f1 = {
225            let prop1 = PropDef::new("p1", &obj_t, &json!({}));
226            FeatureDef::new("test_feature", "documentation", vec![prop1], false)
227        };
228
229        let objs = {
230            let obj_def = ObjectDef::new(
231                obj_nm,
232                &[PropDef::new("obj-p1", &TypeRef::Boolean, &json!(true))],
233            );
234
235            ObjectDef::into_map(&[obj_def])
236        };
237
238        let hasher = DefaultsHasher::new(&objs);
239        // Get an original hash here.
240        let h1 = hasher.hash(&f1);
241
242        // Then change the object later on.
243        let objs = {
244            let obj_def = ObjectDef::new(
245                obj_nm,
246                &[PropDef::new("obj-p1", &TypeRef::Boolean, &json!(false))],
247            );
248
249            ObjectDef::into_map(&[obj_def])
250        };
251
252        let hasher = DefaultsHasher::new(&objs);
253        let ne = hasher.hash(&f1);
254
255        assert_ne!(h1, ne);
256
257        Ok(())
258    }
259
260    #[test]
261    fn test_hash_is_sensitive_to_nested_change() -> Result<()> {
262        let obj1_nm = "MyObject";
263        let obj1_t = TypeRef::Object(obj1_nm.to_string());
264
265        let obj2_nm = "MyNestedObject";
266        let obj2_t = TypeRef::Object(obj2_nm.to_string());
267
268        let obj1_def = ObjectDef::new(obj1_nm, &[PropDef::new("p1-obj2", &obj2_t, &json!({}))]);
269
270        let f1 = {
271            let prop1 = PropDef::new("p1", &obj1_t.clone(), &json!({}));
272            FeatureDef::new("test_feature", "documentation", vec![prop1], false)
273        };
274
275        let objs = {
276            let obj2_def = ObjectDef::new(
277                obj2_nm,
278                &[PropDef::new("p1-string", &TypeRef::String, &json!("one"))],
279            );
280            ObjectDef::into_map(&[obj1_def.clone(), obj2_def])
281        };
282
283        let hasher = DefaultsHasher::new(&objs);
284        // Get an original hash here.
285        let h1 = hasher.hash(&f1);
286
287        // Now change just the deeply nested object.
288        let objs = {
289            let obj2_def = ObjectDef::new(
290                obj2_nm,
291                &[PropDef::new("p1-string", &TypeRef::String, &json!("two"))],
292            );
293            ObjectDef::into_map(&[obj1_def.clone(), obj2_def])
294        };
295        let hasher = DefaultsHasher::new(&objs);
296        let ne = hasher.hash(&f1);
297
298        assert_ne!(h1, ne);
299        Ok(())
300    }
301}