use std::collections::BTreeSet;
use bitflags::bitflags;
use super::{AbstractData, CowData, FieldKey, Patch, Path, SpecData, SpecType, Specifier, Value};
use crate::tf;
#[derive(Debug, Default, Clone)]
pub struct ChangeList {
entries: Vec<(Path, ChangeEntry)>,
}
#[derive(Debug, Default, Clone)]
pub struct ChangeEntry {
pub flags: ChangeFlags,
pub info_changed: BTreeSet<tf::Token>,
}
bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct ChangeFlags: u16 {
const ADD_INERT_PRIM = 1 << 0;
const ADD_NON_INERT_PRIM = 1 << 1;
const REMOVE_INERT_PRIM = 1 << 2;
const REMOVE_NON_INERT_PRIM = 1 << 3;
const ADD_PROPERTY = 1 << 4;
const REMOVE_PROPERTY = 1 << 5;
const CHANGE_RELATIONSHIP_TARGETS = 1 << 6;
const CHANGE_ATTRIBUTE_CONNECTION = 1 << 7;
const NON_INERT_PRIM = Self::ADD_NON_INERT_PRIM.bits() | Self::REMOVE_NON_INERT_PRIM.bits();
const INERT_PRIM = Self::ADD_INERT_PRIM.bits() | Self::REMOVE_INERT_PRIM.bits();
const ADD = Self::ADD_NON_INERT_PRIM.bits() | Self::ADD_INERT_PRIM.bits() | Self::ADD_PROPERTY.bits();
const REMOVE =
Self::REMOVE_NON_INERT_PRIM.bits() | Self::REMOVE_INERT_PRIM.bits() | Self::REMOVE_PROPERTY.bits();
}
}
impl ChangeEntry {
pub fn is_empty(&self) -> bool {
self.flags.is_empty() && self.info_changed.is_empty()
}
pub fn is_child_bookkeeping(&self) -> bool {
self.flags.is_empty() && self.authored_fields().next().is_none()
}
pub fn authored_fields(&self) -> impl Iterator<Item = &tf::Token> {
self.info_changed
.iter()
.filter(|f| !super::is_children_field(f.as_str()))
}
}
impl ChangeList {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn entries(&self) -> &[(Path, ChangeEntry)] {
&self.entries
}
pub fn iter(&self) -> std::slice::Iter<'_, (Path, ChangeEntry)> {
self.entries.iter()
}
pub fn entry_mut(&mut self, path: &Path) -> &mut ChangeEntry {
if let Some(pos) = self.entries.iter().position(|(p, _)| p == path) {
return &mut self.entries[pos].1;
}
self.entries.push((path.clone(), ChangeEntry::default()));
&mut self.entries.last_mut().expect("just pushed").1
}
pub fn merge_from(&mut self, other: &ChangeList) {
for (path, entry) in &other.entries {
let merged = self.entry_mut(path);
merged.flags |= entry.flags;
merged.info_changed.extend(entry.info_changed.iter().cloned());
}
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn from_overlay<T: AbstractData>(cow: &CowData<T>) -> ChangeList {
let mut changes = ChangeList::new();
changes.update(cow);
changes
}
pub(crate) fn update<T: AbstractData>(&mut self, cow: &CowData<T>) {
self.entries.clear();
let base = cow.base();
for (path, patch) in cow.overlay() {
let mut entry = ChangeEntry::default();
match patch {
Patch::Created(spec) => {
if base.has_spec(path) {
note_removal(&mut entry, base, path);
}
match spec.ty {
SpecType::Prim | SpecType::Attribute | SpecType::Relationship => {
entry.flags |= add_flag(spec);
for (field, _) in &spec.fields {
if field != FieldKey::Specifier.as_str() {
note_field(&mut entry, field);
}
}
}
_ => {
for (field, value) in &spec.fields {
if field_changed(base, path, field, value) {
note_field(&mut entry, field);
}
}
}
}
}
Patch::Tombstone => note_removal(&mut entry, base, path),
Patch::Edited { set, erased } => {
for (field, value) in set {
if field_changed(base, path, field, value) {
note_field(&mut entry, field);
}
}
for field in erased {
note_field(&mut entry, field);
}
}
}
if !entry.is_empty() {
self.entries.push((path.clone(), entry));
}
}
}
}
fn note_field(entry: &mut ChangeEntry, field: &str) {
entry.info_changed.insert(tf::Token::from(field));
entry.flags |= flag_for_field(field);
}
fn field_changed(base: &dyn AbstractData, path: &Path, field: &str, value: &Value) -> bool {
base.try_field(path, field).ok().flatten().as_deref() != Some(value)
}
fn note_removal(entry: &mut ChangeEntry, data: &dyn AbstractData, path: &Path) {
let Some(flag) = remove_flag(data, path) else {
return;
};
entry.flags |= flag;
if flag.intersects(ChangeFlags::REMOVE_INERT_PRIM | ChangeFlags::REMOVE_NON_INERT_PRIM) {
if let Some(fields) = data.list_fields(path) {
for field in fields {
if field != FieldKey::Specifier.as_str() {
note_field(entry, &field);
}
}
}
} else {
for field in [FieldKey::TargetPaths.as_str(), FieldKey::ConnectionPaths.as_str()] {
if data.has_field(path, field) {
note_field(entry, field);
}
}
}
}
fn add_flag(spec: &SpecData) -> ChangeFlags {
match spec.ty {
SpecType::Prim => match specifier_of_spec(spec) {
Some(Specifier::Over) | None => ChangeFlags::ADD_INERT_PRIM,
Some(_) => ChangeFlags::ADD_NON_INERT_PRIM,
},
SpecType::Attribute | SpecType::Relationship => ChangeFlags::ADD_PROPERTY,
_ => ChangeFlags::empty(),
}
}
fn as_specifier(value: Option<&Value>) -> Option<Specifier> {
match value {
Some(Value::Specifier(s)) => Some(*s),
_ => None,
}
}
fn specifier_of_spec(spec: &SpecData) -> Option<Specifier> {
as_specifier(spec.get(FieldKey::Specifier.as_str()))
}
fn specifier_of(data: &dyn AbstractData, path: &Path) -> Option<Specifier> {
as_specifier(
data.try_field(path, FieldKey::Specifier.as_str())
.ok()
.flatten()
.as_deref(),
)
}
fn remove_flag(data: &dyn AbstractData, path: &Path) -> Option<ChangeFlags> {
match data.spec_type(path)? {
SpecType::Prim => Some(match specifier_of(data, path) {
Some(Specifier::Over) | None => ChangeFlags::REMOVE_INERT_PRIM,
Some(_) => ChangeFlags::REMOVE_NON_INERT_PRIM,
}),
SpecType::Attribute | SpecType::Relationship => Some(ChangeFlags::REMOVE_PROPERTY),
_ => None,
}
}
fn flag_for_field(field: &str) -> ChangeFlags {
if field == FieldKey::TargetPaths.as_str() {
ChangeFlags::CHANGE_RELATIONSHIP_TARGETS
} else if field == FieldKey::ConnectionPaths.as_str() {
ChangeFlags::CHANGE_ATTRIBUTE_CONNECTION
} else {
ChangeFlags::empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdf::{AttributeSpec, ChildrenKey, Data, PathListOp, PrimSpec, ReferenceListOp, Variability};
fn p(s: &str) -> Path {
Path::new(s).expect("valid path")
}
fn rooted() -> Data {
let mut data = Data::new();
data.create_spec(Path::abs_root(), SpecType::PseudoRoot);
data
}
fn has_flag(cl: &ChangeList, path: &str, flag: ChangeFlags) -> bool {
cl.iter().any(|(pp, e)| pp == &p(path) && e.flags.contains(flag))
}
fn references(field: &str) -> bool {
field == FieldKey::References.as_str()
}
fn specifier(field: &str) -> bool {
field == FieldKey::Specifier.as_str()
}
fn derive(base: Data, author: impl FnOnce(&mut CowData<Data>)) -> ChangeList {
let mut cow = CowData::new(base);
author(&mut cow);
ChangeList::from_overlay(&cow)
}
fn create_over_with_reference(cow: &mut CowData<Data>, path: &Path) {
cow.create_spec(path.clone(), SpecType::Prim);
cow.set_field(path, FieldKey::Specifier.as_str(), Value::Specifier(Specifier::Over));
cow.set_field(
path,
FieldKey::References.as_str(),
Value::ReferenceListOp(ReferenceListOp::default()),
);
}
#[test]
fn merge_combines_same_path() {
let mut a = ChangeList::new();
a.entry_mut(&p("/P")).flags |= ChangeFlags::CHANGE_RELATIONSHIP_TARGETS;
a.entry_mut(&p("/P"))
.info_changed
.insert(FieldKey::TargetPaths.as_str().into());
let mut b = ChangeList::new();
b.entry_mut(&p("/P")).flags |= ChangeFlags::ADD_PROPERTY;
b.entry_mut(&p("/P"))
.info_changed
.insert(FieldKey::ConnectionPaths.as_str().into());
a.merge_from(&b);
let same: Vec<_> = a.entries().iter().filter(|(path, _)| path == &p("/P")).collect();
assert_eq!(same.len(), 1, "the path is merged, not duplicated");
let entry = &same[0].1;
assert!(entry.flags.contains(ChangeFlags::CHANGE_RELATIONSHIP_TARGETS));
assert!(entry.flags.contains(ChangeFlags::ADD_PROPERTY));
assert!(entry.info_changed.iter().any(|t| t == FieldKey::TargetPaths.as_str()));
assert!(entry
.info_changed
.iter()
.any(|t| t == FieldKey::ConnectionPaths.as_str()));
}
#[test]
fn created_over_records_arc_not_specifier() {
let cl = derive(Data::new(), |c| create_over_with_reference(c, &p("/X")));
let entry = &cl.entries()[0].1;
assert!(entry.flags.contains(ChangeFlags::ADD_INERT_PRIM));
assert!(entry.info_changed.iter().any(|t| references(t)));
assert!(!entry.info_changed.iter().any(|t| specifier(t)));
}
#[test]
fn created_over_suppresses_specifier() {
let cl = derive(Data::new(), |c| {
c.create_spec(p("/X"), SpecType::Prim);
c.set_field(
&p("/X"),
FieldKey::Specifier.as_str(),
Value::Specifier(Specifier::Over),
);
});
let entry = &cl.entries()[0].1;
assert!(entry.flags.contains(ChangeFlags::ADD_INERT_PRIM));
assert!(entry.info_changed.is_empty());
}
#[test]
fn add_flag_by_kind() {
let cl = derive(Data::new(), |c| {
c.create_spec(p("/D"), SpecType::Prim);
c.set_field(&p("/D"), FieldKey::Specifier.as_str(), Value::Specifier(Specifier::Def));
c.create_spec(p("/D.attr"), SpecType::Attribute);
});
let prim = cl.iter().find(|(path, _)| path == &p("/D")).expect("prim entry");
assert!(prim.1.flags.contains(ChangeFlags::ADD_NON_INERT_PRIM));
let attr = cl.iter().find(|(path, _)| path == &p("/D.attr")).expect("attr entry");
assert!(attr.1.flags.contains(ChangeFlags::ADD_PROPERTY));
}
#[test]
fn erased_over_records_fields() {
let mut base = Data::new();
base.create_spec(p("/X"), SpecType::Prim);
base.set_field(
&p("/X"),
FieldKey::Specifier.as_str(),
Value::Specifier(Specifier::Over),
);
base.set_field(
&p("/X"),
FieldKey::References.as_str(),
Value::ReferenceListOp(ReferenceListOp::default()),
);
let cl = derive(base, |c| c.erase_spec(&p("/X")));
let entry = &cl.entries()[0].1;
assert!(entry.flags.contains(ChangeFlags::REMOVE_INERT_PRIM));
assert!(entry.info_changed.iter().any(|t| references(t)));
assert!(!entry.info_changed.iter().any(|t| specifier(t)));
}
#[test]
fn erased_relationship_records_targets() {
let mut base = Data::new();
base.create_spec(p("/P.rel"), SpecType::Relationship);
base.set_field(
&p("/P.rel"),
FieldKey::TargetPaths.as_str(),
Value::PathListOp(PathListOp::default()),
);
let cl = derive(base, |c| c.erase_spec(&p("/P.rel")));
let entry = &cl.entries()[0].1;
assert!(entry.flags.contains(ChangeFlags::REMOVE_PROPERTY));
assert!(entry.flags.contains(ChangeFlags::CHANGE_RELATIONSHIP_TARGETS));
assert!(entry.info_changed.iter().any(|t| t == FieldKey::TargetPaths.as_str()));
}
#[test]
fn recreated_over_records_dropped_field() {
let mut base = Data::new();
base.create_spec(p("/X"), SpecType::Prim);
base.set_field(
&p("/X"),
FieldKey::Specifier.as_str(),
Value::Specifier(Specifier::Over),
);
base.set_field(
&p("/X"),
FieldKey::References.as_str(),
Value::ReferenceListOp(ReferenceListOp::default()),
);
let cl = derive(base, |c| {
c.create_spec(p("/X"), SpecType::Prim);
c.set_field(
&p("/X"),
FieldKey::Specifier.as_str(),
Value::Specifier(Specifier::Over),
);
});
let entry = &cl.entries()[0].1;
assert!(entry.flags.contains(ChangeFlags::REMOVE_INERT_PRIM));
assert!(entry.info_changed.iter().any(|t| references(t)));
}
#[test]
fn created_then_erased_cancels() {
let cl = derive(Data::new(), |c| {
create_over_with_reference(c, &p("/X"));
c.erase_spec(&p("/X"));
});
assert!(cl.is_empty());
}
#[test]
fn recreated_then_erased_records_removal() {
let mut base = Data::new();
base.create_spec(p("/X"), SpecType::Prim);
base.set_field(
&p("/X"),
FieldKey::Specifier.as_str(),
Value::Specifier(Specifier::Over),
);
let cl = derive(base, |c| {
c.create_spec(p("/X"), SpecType::Prim);
c.erase_spec(&p("/X"));
});
assert!(cl.entries()[0].1.flags.contains(ChangeFlags::REMOVE_INERT_PRIM));
}
#[test]
fn target_and_connection_flags() {
let mut base = Data::new();
base.create_spec(p("/P.rel"), SpecType::Relationship);
base.create_spec(p("/P.attr"), SpecType::Attribute);
let cl = derive(base, |c| {
c.set_field(
&p("/P.rel"),
FieldKey::TargetPaths.as_str(),
Value::PathListOp(PathListOp::default()),
);
c.set_field(
&p("/P.attr"),
FieldKey::ConnectionPaths.as_str(),
Value::PathListOp(PathListOp::default()),
);
});
let rel = cl.iter().find(|(path, _)| path == &p("/P.rel")).expect("rel entry");
assert!(rel.1.flags.contains(ChangeFlags::CHANGE_RELATIONSHIP_TARGETS));
let attr = cl.iter().find(|(path, _)| path == &p("/P.attr")).expect("attr entry");
assert!(attr.1.flags.contains(ChangeFlags::CHANGE_ATTRIBUTE_CONNECTION));
}
#[test]
fn idempotent_set_no_change() {
let mut base = Data::new();
base.create_spec(p("/P"), SpecType::Prim);
base.set_field(
&p("/P"),
FieldKey::TypeName.as_str(),
Value::Token(tf::Token::from("Xform")),
);
let cl = derive(base, |c| {
c.set_field(
&p("/P"),
FieldKey::TypeName.as_str(),
Value::Token(tf::Token::from("Xform")),
);
});
assert!(cl.is_empty());
}
#[test]
fn add_then_erase_field_no_change() {
let mut base = Data::new();
base.create_spec(p("/P"), SpecType::Prim);
let cl = derive(base, |c| {
c.set_field(
&p("/P"),
FieldKey::TypeName.as_str(),
Value::Token(tf::Token::from("Xform")),
);
c.erase_field(&p("/P"), FieldKey::TypeName.as_str());
});
assert!(cl.is_empty());
}
#[test]
fn child_name_only_is_bookkeeping() {
let mut base = Data::new();
base.create_spec(p("/P"), SpecType::Prim);
let cl = derive(base, |c| {
c.set_field(
&p("/P"),
ChildrenKey::PrimChildren.as_str(),
Value::TokenVec(vec![tf::Token::from("Child")]),
);
});
assert!(cl.entries()[0].1.is_child_bookkeeping());
}
#[test]
fn records_prim_tree_adds() {
let cl = derive(rooted(), |c| {
PrimSpec::new(c, "/A/B/C", Specifier::Def, "Xform").unwrap();
AttributeSpec::new(c, "/A/B/C.size", "double", Variability::Varying, true).unwrap();
});
assert!(has_flag(&cl, "/A", ChangeFlags::ADD_INERT_PRIM));
assert!(has_flag(&cl, "/A/B", ChangeFlags::ADD_INERT_PRIM));
assert!(has_flag(&cl, "/A/B/C", ChangeFlags::ADD_NON_INERT_PRIM));
assert!(has_flag(&cl, "/A/B/C.size", ChangeFlags::ADD_PROPERTY));
let leaf = &cl.iter().find(|(pp, _)| pp == &p("/A/B/C")).unwrap().1.info_changed;
assert!(leaf.iter().any(|t| t == FieldKey::TypeName.as_str()));
assert!(!leaf.iter().any(|t| specifier(t)));
}
#[test]
fn over_records_inert() {
let cl = derive(rooted(), |c| {
PrimSpec::over(c, "/X/Y").unwrap();
});
assert!(has_flag(&cl, "/X", ChangeFlags::ADD_INERT_PRIM));
assert!(has_flag(&cl, "/X/Y", ChangeFlags::ADD_INERT_PRIM));
}
#[test]
fn metadata_on_existing_prim() {
let mut base = rooted();
PrimSpec::new(&mut base, "/A", Specifier::Def, "").unwrap();
let cl = derive(base, |c| {
c.set_field(&p("/A"), FieldKey::Kind.as_str(), Value::token("component"));
});
let e = &cl.iter().find(|(pp, _)| pp == &p("/A")).unwrap().1;
assert!(e.flags.is_empty());
assert!(e.info_changed.iter().any(|t| t == FieldKey::Kind.as_str()));
}
#[test]
fn metadata_materializes_pseudo_root() {
let cl = derive(Data::new(), |c| {
c.create_spec(Path::abs_root(), SpecType::PseudoRoot);
c.set_field(&Path::abs_root(), FieldKey::DefaultPrim.as_str(), Value::token("World"));
});
let e = &cl.iter().find(|(pp, _)| pp.is_abs_root()).unwrap().1;
assert!(e.info_changed.iter().any(|t| t == FieldKey::DefaultPrim.as_str()));
}
#[test]
fn redundant_define_no_change() {
let mut base = rooted();
PrimSpec::new(&mut base, "/A", Specifier::Def, "Xform").unwrap();
let cl = derive(base, |c| {
PrimSpec::new(c, "/A", Specifier::Def, "Xform").unwrap();
});
assert!(cl.is_empty());
}
}