Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement instance cloning methods in WeakDom #312

Merged
merged 25 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b7e5c26
Preliminary implementation of clone_within and clone_external
kennethloeffler Jul 5, 2023
bdbaeb2
Rename CloneContext.dom -> CloneContext.source
kennethloeffler Jul 5, 2023
378ff0d
Add tests
kennethloeffler Jul 5, 2023
74bedb5
Rename clone_external -> clone_into_external
kennethloeffler Jul 5, 2023
7352f7b
Add snapshots
kennethloeffler Jul 5, 2023
d0ee591
Refactor: CloneContext is now POD, move implementations into WeakDom
kennethloeffler Jul 5, 2023
1643fd5
Add a little docs
kennethloeffler Jul 5, 2023
6fdb764
Rename instancebuilder_from_ref -> ref_into_instancebuilder
kennethloeffler Jul 5, 2023
b888d10
Refactor: ref_into_instancebuilder simplification
kennethloeffler Jul 5, 2023
c6c90a6
Rename old_ref -> original_ref
kennethloeffler Jul 5, 2023
046ebc0
Consume CloneContext in rewrite_refs
kennethloeffler Jul 5, 2023
7b94456
ref_into_instancebuiler->clone_ref_as_builder, add a little docs
kennethloeffler Jul 6, 2023
5e32765
Refactor: WeakDom::rewrite_refs -> CloneContext::rewrite_refs
kennethloeffler Jul 6, 2023
fc26bdc
Move CloneContext down below WeakDom
kennethloeffler Jul 6, 2023
85f49a8
Refactor: move WeakDom::clone_ref_as_builder into CloneContext
kennethloeffler Jul 6, 2023
0c6a90e
Clarify the docs
kennethloeffler Jul 6, 2023
1ba6eab
Docs docs docs
kennethloeffler Jul 6, 2023
a253fef
Set ref properties to none if they DNE in the destination WeakDom
kennethloeffler Jul 7, 2023
7a88c91
Edit clone_into_external test to show refs that DNE are set to none
kennethloeffler Jul 7, 2023
040df5e
Put (valid) ref pointing outside the subtree in clone_within test
kennethloeffler Jul 7, 2023
aafe32c
Correctly rewrite refs that point outside subtree in rewrite_refs
kennethloeffler Jul 7, 2023
e16aeaa
Don't need this type annotation
kennethloeffler Jul 7, 2023
e2bab9a
Mention the out-of-subtree ref case in clone_into_external docs
kennethloeffler Jul 7, 2023
2da1e4b
Add changelog entry
kennethloeffler Jul 7, 2023
797954b
Fix typo in rewrite_refs comment
kennethloeffler Jul 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions rbx_dom_weak/src/dom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,48 @@ impl WeakDom {
dest_parent.children.push(referent);
}

/// Clone the instance with the given `referent` and all its descendants
/// (i.e. the entire subtree) into the same WeakDom.
///
/// After the operation, the root of the cloned subtree has no parent.
///
/// Any Ref properties that point to instances contained in the subtree are
/// rewritten to point to the cloned instances.
pub fn clone_within(&mut self, referent: Ref) -> Ref {
let mut ctx = CloneContext::default();
let root_builder = ctx.clone_ref_as_builder(self, referent);
let root_ref = self.insert(Ref::none(), root_builder);

while let Some((cloned_parent, uncloned_child)) = ctx.queue.pop_front() {
let builder = ctx.clone_ref_as_builder(self, uncloned_child);
self.insert(cloned_parent, builder);
}

ctx.rewrite_refs(self);
root_ref
}

/// Clone the instance with the given `referent` and all its descendants (i.e. the
/// entire subtree) into the given WeakDom.
///
/// After the operation, the root of the cloned subtree has no parent.
///
/// Any Ref properties that point to instances contained in the subtree are
/// rewritten to point to the cloned instances.
kennethloeffler marked this conversation as resolved.
Show resolved Hide resolved
pub fn clone_into_external(&self, referent: Ref, dest: &mut WeakDom) -> Ref {
let mut ctx = CloneContext::default();
let root_builder = ctx.clone_ref_as_builder(self, referent);
let root_ref = dest.insert(Ref::none(), root_builder);

while let Some((cloned_parent, uncloned_child)) = ctx.queue.pop_front() {
let builder = ctx.clone_ref_as_builder(self, uncloned_child);
dest.insert(cloned_parent, builder);
}

ctx.rewrite_refs(dest);
kennethloeffler marked this conversation as resolved.
Show resolved Hide resolved
root_ref
}

fn inner_insert(&mut self, referent: Ref, instance: Instance) {
self.instances.insert(referent, instance);

Expand Down Expand Up @@ -279,6 +321,59 @@ impl WeakDom {
}
}

#[derive(Debug, Default)]
struct CloneContext {
queue: VecDeque<(Ref, Ref)>,
ref_rewrites: HashMap<Ref, Ref>,
}

impl CloneContext {
/// On any instances cloned during the operation, rewrite any Ref properties that
/// point to instances that were also cloned.
fn rewrite_refs(self, dom: &mut WeakDom) {
for (_, new_ref) in self.ref_rewrites.iter() {
let instance = dom
.get_by_ref_mut(*new_ref)
.expect("Cannot rewrite refs on an instance that does not exist");

for prop_value in instance.properties.values_mut() {
if let Variant::Ref(original_ref) = prop_value {
// We only want to rewrite Refs if they point to instances within the
// cloned subtree
if let Some(new_ref) = self.ref_rewrites.get(original_ref) {
*prop_value = Variant::Ref(*new_ref);
}
}
}
}
}

/// Clone the instance with the given `referent` and `source` WeakDom into a new
/// InstanceBuilder, and record the mapping of the original referent to the new
/// referent.
///
/// This method only clones the instance's class name, name, and properties; it
/// does not clone any children.
fn clone_ref_as_builder(&mut self, source: &WeakDom, original_ref: Ref) -> InstanceBuilder {
let instance = source
.get_by_ref(original_ref)
.expect("Cannot clone an instance that does not exist");

let builder = InstanceBuilder::new(instance.class.to_string())
.with_name(instance.name.to_string())
.with_properties(instance.properties.clone());

let new_ref = builder.referent;

for uncloned_child in instance.children.iter() {
self.queue.push_back((new_ref, *uncloned_child))
}

self.ref_rewrites.insert(original_ref, new_ref);
builder
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down Expand Up @@ -343,6 +438,75 @@ mod test {
insta::assert_yaml_snapshot!(viewer.view_children(&dom));
}

#[test]
fn clone_within() {
let mut dom = {
let mut child1 = InstanceBuilder::new("Part");
let mut child2 = InstanceBuilder::new("Part");

child1 = child1.with_property("RefProp", child2.referent);
child2 = child2.with_property("RefProp", child1.referent);

WeakDom::new(
InstanceBuilder::new("Folder")
.with_name("Root")
.with_children([child1, child2]),
)
};

let cloned_root = dom.clone_within(dom.root_ref);

assert!(
dom.get_by_ref(cloned_root).unwrap().parent.is_none(),
"parent of cloned subtree root should be none directly after a clone"
);

dom.transfer_within(cloned_root, dom.root_ref);

// This snapshot should have a clone of the root Folder under itself, with the ref
// properties in the cloned subtree pointing to the cloned instances.
let mut viewer = DomViewer::new();
insta::assert_yaml_snapshot!(viewer.view(&dom));
}

#[test]
fn clone_into_external() {
let dom = {
let mut child1 = InstanceBuilder::new("Part");
let mut child2 = InstanceBuilder::new("Part");

child1 = child1.with_property("RefProp", child2.referent);
child2 = child2.with_property("RefProp", child1.referent);

WeakDom::new(
InstanceBuilder::new("Folder")
.with_name("Root")
.with_children([child1, child2]),
)
};

let mut other_dom = WeakDom::new(InstanceBuilder::new("DataModel"));
let cloned_root = dom.clone_into_external(dom.root_ref, &mut other_dom);

assert!(
other_dom.get_by_ref(cloned_root).unwrap().parent.is_none(),
"parent of cloned subtree root should be none directly after a clone"
);

other_dom.transfer_within(cloned_root, other_dom.root_ref);

let mut viewer = DomViewer::new();

// This snapshot is here just to show that the ref props are rewritten after being
// cloned into the other dom. It should contain a Folder at the root with two
// Parts as children
insta::assert_yaml_snapshot!(viewer.view(&dom));

// This snapshot should have a clone of the root Folder under the other dom's
// DataModel, with the ref properties pointing to the newly cloned parts.
insta::assert_yaml_snapshot!(viewer.view(&other_dom));
}

#[test]
fn large_depth_tree() {
// We've had issues with stack overflows when creating WeakDoms with
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
source: rbx_dom_weak/src/dom.rs
expression: viewer.view(&other_dom)
---
referent: referent-3
name: DataModel
class: DataModel
properties: {}
children:
- referent: referent-4
name: Root
class: Folder
properties: {}
children:
- referent: referent-5
name: Part
class: Part
properties:
RefProp: referent-6
children: []
- referent: referent-6
name: Part
class: Part
properties:
RefProp: referent-5
children: []

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
source: rbx_dom_weak/src/dom.rs
expression: viewer.view(&dom)
---
referent: referent-0
name: Root
class: Folder
properties: {}
children:
- referent: referent-1
name: Part
class: Part
properties:
RefProp: referent-2
children: []
- referent: referent-2
name: Part
class: Part
properties:
RefProp: referent-1
children: []

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
source: rbx_dom_weak/src/dom.rs
expression: viewer.view(&dom)
---
referent: referent-0
name: Root
class: Folder
properties: {}
children:
- referent: referent-1
name: Part
class: Part
properties:
RefProp: referent-2
children: []
- referent: referent-2
name: Part
class: Part
properties:
RefProp: referent-1
children: []
- referent: referent-3
name: Root
class: Folder
properties: {}
children:
- referent: referent-4
name: Part
class: Part
properties:
RefProp: referent-5
children: []
- referent: referent-5
name: Part
class: Part
properties:
RefProp: referent-4
children: []