#![forbid(unsafe_code)]
use cyrs_schema::{
Cardinality, EmptySchema, EndpointDecl, FunctionSignature, ProcedureSignature, PropertyDecl,
PropertyType, SchemaProvider,
};
use sha2::{Digest, Sha256};
use smol_str::SmolStr;
#[derive(Debug, Default, Clone)]
struct TestSchema {
labels: Vec<String>,
rel_types: Vec<String>,
node_props: Vec<(String, Vec<PropertyDecl>)>,
rel_endpoints: Vec<(String, Vec<EndpointDecl>)>,
}
impl SchemaProvider for TestSchema {
fn labels(&self) -> Vec<SmolStr> {
self.labels.iter().map(|s| s.as_str().into()).collect()
}
fn relationship_types(&self) -> Vec<SmolStr> {
self.rel_types.iter().map(|s| s.as_str().into()).collect()
}
fn has_label(&self, name: &str) -> bool {
self.labels.iter().any(|s| s == name)
}
fn has_relationship_type(&self, name: &str) -> bool {
self.rel_types.iter().any(|s| s == name)
}
fn node_properties(&self, label: &str) -> Option<Vec<PropertyDecl>> {
self.node_props
.iter()
.find(|(n, _)| n == label)
.map(|(_, ps)| ps.clone())
}
fn relationship_properties(&self, _rel_type: &str) -> Option<Vec<PropertyDecl>> {
None
}
fn relationship_endpoints(&self, rel_type: &str) -> Vec<EndpointDecl> {
self.rel_endpoints
.iter()
.find(|(n, _)| n == rel_type)
.map(|(_, e)| e.clone())
.unwrap_or_default()
}
fn inverse_of(&self, _rel_type: &str) -> Option<SmolStr> {
None
}
fn function(&self, _name: &str) -> Option<FunctionSignature> {
None
}
fn procedure(&self, _name: &str) -> Option<ProcedureSignature> {
None
}
fn schema_digest(&self) -> [u8; 32] {
let mut h = Sha256::new();
h.update([0x01]);
let mut ls: Vec<String> = self.labels.clone();
ls.sort();
for l in &ls {
h.update(l.as_bytes());
h.update([0x00]);
}
h.update([0x02]);
let mut rs: Vec<String> = self.rel_types.clone();
rs.sort();
for r in &rs {
h.update(r.as_bytes());
h.update([0x00]);
}
h.update([0x03]);
let mut np = self.node_props.clone();
np.sort_by(|a, b| a.0.cmp(&b.0));
for (label, props) in &np {
h.update(label.as_bytes());
h.update([0x00]);
let mut ps = props.clone();
ps.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
for p in &ps {
h.update(p.name.as_bytes());
h.update([0x00]);
feed_property_type(&mut h, &p.ty);
h.update([u8::from(p.required)]);
}
h.update([0x1e]); }
h.update([0x04]);
let mut re = self.rel_endpoints.clone();
re.sort_by(|a, b| a.0.cmp(&b.0));
for (rel, ends) in &re {
h.update(rel.as_bytes());
h.update([0x00]);
let mut ends_sorted = ends.clone();
ends_sorted.sort_by(|a, b| {
(a.from.as_str(), a.to.as_str()).cmp(&(b.from.as_str(), b.to.as_str()))
});
for e in &ends_sorted {
h.update(e.from.as_bytes());
h.update([0x00]);
h.update(e.to.as_bytes());
h.update([0x00]);
h.update([card_tag(e.cardinality)]);
}
h.update([0x1e]);
}
h.finalize().into()
}
}
fn feed_property_type(h: &mut Sha256, t: &PropertyType) {
match t {
PropertyType::String => h.update([1]),
PropertyType::Int => h.update([2]),
PropertyType::Float => h.update([3]),
PropertyType::Bool => h.update([4]),
PropertyType::Date => h.update([5]),
PropertyType::Datetime => h.update([6]),
PropertyType::List(inner) => {
h.update([7]);
feed_property_type(h, inner);
}
PropertyType::Enum(name, variants) => {
h.update([8]);
h.update(name.as_bytes());
h.update([0x00]);
let mut vs: Vec<SmolStr> = variants.clone();
vs.sort();
for v in &vs {
h.update(v.as_bytes());
h.update([0x00]);
}
h.update([0x1f]);
}
PropertyType::Opaque(name) => {
h.update([9]);
h.update(name.as_bytes());
h.update([0x00]);
}
PropertyType::Any => h.update([10]),
}
}
fn card_tag(c: Cardinality) -> u8 {
match c {
Cardinality::OneToOne => 1,
Cardinality::OneToMany => 2,
Cardinality::ManyToOne => 3,
Cardinality::ManyToMany => 4,
_ => 255,
}
}
#[test]
fn empty_schema_digest_is_deterministic() {
assert_eq!(EmptySchema.schema_digest(), EmptySchema.schema_digest());
}
#[test]
fn identical_schemas_produce_identical_digests() {
let a = TestSchema {
labels: vec!["Person".into(), "Company".into()],
rel_types: vec!["KNOWS".into()],
..Default::default()
};
let b = a.clone();
assert_eq!(a.schema_digest(), b.schema_digest());
}
#[test]
fn label_order_does_not_affect_digest() {
let a = TestSchema {
labels: vec!["Person".into(), "Company".into()],
..Default::default()
};
let b = TestSchema {
labels: vec!["Company".into(), "Person".into()],
..Default::default()
};
assert_eq!(a.schema_digest(), b.schema_digest());
}
#[test]
fn adding_a_label_changes_digest() {
let mut a = TestSchema {
labels: vec!["Person".into()],
..Default::default()
};
let d1 = a.schema_digest();
a.labels.push("Company".into());
let d2 = a.schema_digest();
assert_ne!(d1, d2);
}
#[test]
fn adding_a_relationship_type_changes_digest() {
let mut a = TestSchema::default();
let d1 = a.schema_digest();
a.rel_types.push("KNOWS".into());
let d2 = a.schema_digest();
assert_ne!(d1, d2);
}
#[test]
fn adding_a_node_property_changes_digest() {
let mut a = TestSchema {
labels: vec!["Person".into()],
..Default::default()
};
let d1 = a.schema_digest();
a.node_props.push((
"Person".into(),
vec![PropertyDecl::new("name", PropertyType::String, true)],
));
let d2 = a.schema_digest();
assert_ne!(d1, d2);
}
#[test]
fn changing_property_type_changes_digest() {
let mut a = TestSchema {
labels: vec!["Person".into()],
node_props: vec![(
"Person".into(),
vec![PropertyDecl::new("age", PropertyType::Int, false)],
)],
..Default::default()
};
let d1 = a.schema_digest();
a.node_props[0].1[0].ty = PropertyType::String;
let d2 = a.schema_digest();
assert_ne!(d1, d2);
}
#[test]
fn toggling_required_changes_digest() {
let mut a = TestSchema {
labels: vec!["Person".into()],
node_props: vec![(
"Person".into(),
vec![PropertyDecl::new("name", PropertyType::String, false)],
)],
..Default::default()
};
let d1 = a.schema_digest();
a.node_props[0].1[0].required = true;
let d2 = a.schema_digest();
assert_ne!(d1, d2);
}
#[test]
fn adding_rel_endpoint_changes_digest() {
let mut a = TestSchema {
rel_types: vec!["KNOWS".into()],
..Default::default()
};
let d1 = a.schema_digest();
a.rel_endpoints.push((
"KNOWS".into(),
vec![EndpointDecl {
from: "Person".into(),
to: "Person".into(),
cardinality: Cardinality::ManyToMany,
}],
));
let d2 = a.schema_digest();
assert_ne!(d1, d2);
}