Skip to content

Commit

Permalink
Rename structure --> schema
Browse files Browse the repository at this point in the history
  • Loading branch information
jhugman committed Nov 29, 2023
1 parent b518395 commit 7e13a2f
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 78 deletions.
4 changes: 2 additions & 2 deletions components/support/nimbus-fml/src/client/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ impl FmlFeatureInspector {
self.get_first_error(string).map(|e| vec![e])
}

pub fn get_structure_hash(&self) -> String {
pub fn get_schema_hash(&self) -> String {
self.manifest
.get_structure_hash(&self.feature_id)
.get_schema_hash(&self.feature_id)
.unwrap_or_default()
}
}
Expand Down
305 changes: 305 additions & 0 deletions components/support/nimbus-fml/src/defaults/hasher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use crate::schema::TypeQuery;
use crate::{
intermediate_representation::{FeatureDef, ObjectDef, PropDef, TypeRef},
schema::Sha256Hasher,
};
use serde_json::Value;
use std::{
collections::{BTreeMap, BTreeSet, HashSet},
hash::{Hash, Hasher},
};

pub(crate) struct DefaultsHasher<'a> {
object_defs: &'a BTreeMap<String, ObjectDef>,
}

impl<'a> DefaultsHasher<'a> {
pub(crate) fn new(objs: &'a BTreeMap<String, ObjectDef>) -> Self {
Self { object_defs: objs }
}

pub(crate) fn hash(&self, feature_def: &FeatureDef) -> u64 {
let mut hasher: Sha256Hasher = Default::default();
feature_def.defaults_hash(&mut hasher);

let types = self.all_types(feature_def);

// We iterate through the object_defs because they are both
// ordered, and we want to maintain a stable ordering.
// By contrast, `types`, a HashSet, definitely does not have a stable ordering.
for (obj_nm, obj_def) in self.object_defs {
if types.contains(&TypeRef::Object(obj_nm.clone())) {
obj_def.defaults_hash(&mut hasher);
}
}

hasher.finish()
}

fn all_types(&self, feature_def: &FeatureDef) -> HashSet<TypeRef> {
let types = TypeQuery::new(self.object_defs);
types.all_types(feature_def)
}
}

trait DefaultsHash {
fn defaults_hash<H: Hasher>(&self, state: &mut H);
}

impl DefaultsHash for FeatureDef {
fn defaults_hash<H: Hasher>(&self, state: &mut H) {
self.props.defaults_hash(state);
}
}

impl DefaultsHash for Vec<PropDef> {
fn defaults_hash<H: Hasher>(&self, state: &mut H) {
let mut vec: Vec<&_> = Default::default();
for item in self {
vec.push(item);
}
vec.sort_by_key(|item| &item.name);

for item in vec {
item.defaults_hash(state);
}
}
}

impl DefaultsHash for PropDef {
fn defaults_hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.default.defaults_hash(state);
}
}

impl DefaultsHash for ObjectDef {
fn defaults_hash<H: Hasher>(&self, state: &mut H) {
self.props.defaults_hash(state);
}
}

impl DefaultsHash for Value {
fn defaults_hash<H: Hasher>(&self, state: &mut H) {
match self {
Self::Null => Option::<bool>::None.hash(state),
Self::Number(v) => v.hash(state),
Self::Bool(v) => v.hash(state),
Self::String(v) => v.hash(state),
Self::Array(array) => {
for v in array {
v.defaults_hash(state);
}
}
Self::Object(map) => {
let keys = map.keys().collect::<BTreeSet<_>>();
for k in keys {
let v = map.get(k).unwrap();
v.defaults_hash(state);
}
}
}
}
}

#[cfg(test)]
mod unit_tests {
use super::*;
use crate::error::Result;

use serde_json::json;

#[test]
fn test_simple_feature_stable_over_time() -> Result<()> {
let objs = Default::default();

let feature_def = {
let p1 = PropDef::new("my-int", &TypeRef::Int, &json!(1));
let p2 = PropDef::new("my-bool", &TypeRef::Boolean, &json!(true));
let p3 = PropDef::new("my-string", &TypeRef::String, &json!("string"));
FeatureDef::new("test_feature", "", vec![p1, p2, p3], false)
};

let mut prev: Option<u64> = None;
for _ in 0..100 {
let hasher = DefaultsHasher::new(&objs);
let hash = hasher.hash(&feature_def);
if let Some(prev) = prev {
assert_eq!(prev, hash);
}
prev = Some(hash);
}

Ok(())
}

#[test]
fn test_simple_feature_is_stable_with_props_in_any_order() -> Result<()> {
let objs = Default::default();

let p1 = PropDef::new("my-int", &TypeRef::Int, &json!(1));
let p2 = PropDef::new("my-bool", &TypeRef::Boolean, &json!(true));
let p3 = PropDef::new("my-string", &TypeRef::String, &json!("string"));

let f1 = FeatureDef::new(
"test_feature",
"",
vec![p1.clone(), p2.clone(), p3.clone()],
false,
);
let f2 = FeatureDef::new("test_feature", "", vec![p3, p2, p1], false);

let hasher = DefaultsHasher::new(&objs);
assert_eq!(hasher.hash(&f1), hasher.hash(&f2));
Ok(())
}

#[test]
fn test_simple_feature_is_stable_changing_types() -> Result<()> {
let objs = Default::default();

// unsure how you'd do this.
let f1 = {
let prop1 = PropDef::new("p1", &TypeRef::Int, &json!(42));
let prop2 = PropDef::new("p2", &TypeRef::String, &json!("Yes"));
FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
};

let f2 = {
let prop1 = PropDef::new("p1", &TypeRef::String, &json!(42));
let prop2 = PropDef::new("p2", &TypeRef::Int, &json!("Yes"));
FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
};

let hasher = DefaultsHasher::new(&objs);
assert_eq!(hasher.hash(&f1), hasher.hash(&f2));

Ok(())
}

#[test]
fn test_simple_feature_is_sensitive_to_change() -> Result<()> {
let objs = Default::default();

let f1 = {
let prop1 = PropDef::new("p1", &TypeRef::String, &json!("Yes"));
let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(1));
FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
};

let hasher = DefaultsHasher::new(&objs);

// Sensitive to change in type of properties
let ne = {
let prop1 = PropDef::new("p1", &TypeRef::String, &json!("Nope"));
let prop2 = PropDef::new("p2", &TypeRef::Boolean, &json!(1));
FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
};
assert_ne!(hasher.hash(&f1), hasher.hash(&ne));

// Sensitive to change in name of properties
let ne = {
let prop1 = PropDef::new("p1_", &TypeRef::String, &json!("Yes"));
let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(1));
FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
};
assert_ne!(hasher.hash(&f1), hasher.hash(&ne));

// Not Sensitive to change in changes in coenrollment status
let eq = {
let prop1 = PropDef::new("p1", &TypeRef::String, &json!("Yes"));
let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(1));
FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], true)
};
assert_eq!(hasher.hash(&f1), hasher.hash(&eq));

Ok(())
}

#[test]
fn test_feature_is_sensitive_to_object_change() -> Result<()> {
let obj_nm = "MyObject";
let obj_t = TypeRef::Object(obj_nm.to_string());

let f1 = {
let prop1 = PropDef::new("p1", &obj_t, &json!({}));
FeatureDef::new("test_feature", "documentation", vec![prop1], false)
};

let objs = {
let obj_def = ObjectDef::new(
obj_nm,
&[PropDef::new("obj-p1", &TypeRef::Boolean, &json!(true))],
);

ObjectDef::into_map(&[obj_def])
};

let hasher = DefaultsHasher::new(&objs);
// Get an original hash here.
let h1 = hasher.hash(&f1);

// Then change the object later on.
let objs = {
let obj_def = ObjectDef::new(
obj_nm,
&[PropDef::new("obj-p1", &TypeRef::Boolean, &json!(false))],
);

ObjectDef::into_map(&[obj_def])
};

let hasher = DefaultsHasher::new(&objs);
let ne = hasher.hash(&f1);

assert_ne!(h1, ne);

Ok(())
}

#[test]
fn test_hash_is_sensitive_to_nested_change() -> Result<()> {
let obj1_nm = "MyObject";
let obj1_t = TypeRef::Object(obj1_nm.to_string());

let obj2_nm = "MyNestedObject";
let obj2_t = TypeRef::Object(obj2_nm.to_string());

let obj1_def = ObjectDef::new(obj1_nm, &[PropDef::new("p1-obj2", &obj2_t, &json!({}))]);

let f1 = {
let prop1 = PropDef::new("p1", &obj1_t.clone(), &json!({}));
FeatureDef::new("test_feature", "documentation", vec![prop1], false)
};

let objs = {
let obj2_def = ObjectDef::new(
obj2_nm,
&[PropDef::new("p1-string", &TypeRef::String, &json!("one"))],
);
ObjectDef::into_map(&[obj1_def.clone(), obj2_def])
};

let hasher = DefaultsHasher::new(&objs);
// Get an original hash here.
let h1 = hasher.hash(&f1);

// Now change just the deeply nested object.
let objs = {
let obj2_def = ObjectDef::new(
obj2_nm,
&[PropDef::new("p1-string", &TypeRef::String, &json!("two"))],
);
ObjectDef::into_map(&[obj1_def.clone(), obj2_def])
};
let hasher = DefaultsHasher::new(&objs);
let ne = hasher.hash(&f1);

assert_ne!(h1, ne);
Ok(())
}
}
2 changes: 1 addition & 1 deletion components/support/nimbus-fml/src/fml.udl
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ interface FmlFeatureInspector {
// If this hash changes for a given feature then it is almost
// certain that the code which uses this configuration will also have
// changed.
string get_structure_hash();
string get_schema_hash();
};

dictionary FmlEditorError {
Expand Down
12 changes: 6 additions & 6 deletions components/support/nimbus-fml/src/intermediate_representation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::defaults::{DefaultsMerger, DefaultsValidator};
use crate::error::FMLError::InvalidFeatureError;
use crate::error::{FMLError, Result};
use crate::frontend::{AboutBlock, FeatureMetadata};
use crate::structure::{StructureHasher, StructureValidator};
use crate::schema::{SchemaHasher, SchemaValidator};
use crate::util::loaders::FilePath;
use anyhow::{bail, Error, Result as AnyhowResult};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -315,7 +315,7 @@ impl FeatureManifest {
}

fn validate_structure(&self) -> Result<(), FMLError> {
let validator = StructureValidator::new(&self.enum_defs, &self.obj_defs);
let validator = SchemaValidator::new(&self.enum_defs, &self.obj_defs);
for object in self.iter_object_defs() {
validator.validate_object_def(object)?;
}
Expand Down Expand Up @@ -457,18 +457,18 @@ impl FeatureManifest {
Ok(feature_def)
}

pub fn get_structure_hash(&self, feature_name: &str) -> Result<String> {
pub fn get_schema_hash(&self, feature_name: &str) -> Result<String> {
let (manifest, feature_def) = self
.find_feature(feature_name)
.ok_or_else(|| InvalidFeatureError(feature_name.to_string()))?;

Ok(manifest.feature_structure_hash(feature_def))
Ok(manifest.feature_schema_hash(feature_def))
}
}

impl FeatureManifest {
pub(crate) fn feature_structure_hash(&self, feature_def: &FeatureDef) -> String {
let hasher = StructureHasher::new(&self.enum_defs, &self.obj_defs);
pub(crate) fn feature_schema_hash(&self, feature_def: &FeatureDef) -> String {
let hasher = SchemaHasher::new(&self.enum_defs, &self.obj_defs);
let hash = hasher.hash(feature_def) & 0xffffffff;
format!("{hash:x}")
}
Expand Down
2 changes: 1 addition & 1 deletion components/support/nimbus-fml/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub mod error;
pub(crate) mod frontend;
pub mod intermediate_representation;
pub mod parser;
pub(crate) mod structure;
pub(crate) mod schema;
pub mod util;

cfg_if::cfg_if! {
Expand Down
2 changes: 1 addition & 1 deletion components/support/nimbus-fml/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mod fixtures;
mod frontend;
mod intermediate_representation;
mod parser;
mod structure;
mod schema;
mod util;

use anyhow::Result;
Expand Down

0 comments on commit 7e13a2f

Please sign in to comment.