Skip to main content

librojo/syncback/
snapshot.rs

1use indexmap::IndexMap;
2use memofs::Vfs;
3use std::path::{Path, PathBuf};
4
5use crate::{
6    snapshot::{InstanceWithMeta, RojoTree},
7    snapshot_middleware::Middleware,
8    Project,
9};
10use rbx_dom_weak::{
11    types::{Ref, Variant},
12    Instance, Ustr, UstrMap, WeakDom,
13};
14
15use super::{get_best_middleware, name_for_inst, property_filter::filter_properties};
16
17#[derive(Clone, Copy)]
18pub struct SyncbackData<'sync> {
19    pub(super) vfs: &'sync Vfs,
20    pub(super) old_tree: &'sync RojoTree,
21    pub(super) new_tree: &'sync WeakDom,
22    pub(super) project: &'sync Project,
23}
24
25pub struct SyncbackSnapshot<'sync> {
26    pub data: SyncbackData<'sync>,
27    pub old: Option<Ref>,
28    pub new: Ref,
29    pub path: PathBuf,
30    pub middleware: Option<Middleware>,
31}
32
33impl<'sync> SyncbackSnapshot<'sync> {
34    /// Constructs a SyncbackSnapshot from the provided refs
35    /// while inheriting this snapshot's path and data. This should be used for
36    /// directories.
37    #[inline]
38    pub fn with_joined_path(&self, new_ref: Ref, old_ref: Option<Ref>) -> anyhow::Result<Self> {
39        let mut snapshot = Self {
40            data: self.data,
41            old: old_ref,
42            new: new_ref,
43            path: PathBuf::new(),
44            middleware: None,
45        };
46        let middleware = get_best_middleware(&snapshot);
47        let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?;
48        snapshot.path = self.path.join(name.as_ref());
49
50        Ok(snapshot)
51    }
52
53    /// Constructs a SyncbackSnapshot from the provided refs and a base path,
54    /// while inheriting this snapshot's data.
55    ///
56    /// The actual path of the snapshot is made by getting a file name for the
57    /// snapshot and then appending it to the provided base path.
58    #[inline]
59    pub fn with_base_path(
60        &self,
61        base_path: &Path,
62        new_ref: Ref,
63        old_ref: Option<Ref>,
64    ) -> anyhow::Result<Self> {
65        let mut snapshot = Self {
66            data: self.data,
67            old: old_ref,
68            new: new_ref,
69            path: PathBuf::new(),
70            middleware: None,
71        };
72        let middleware = get_best_middleware(&snapshot);
73        let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?;
74        snapshot.path = base_path.join(name.as_ref());
75
76        Ok(snapshot)
77    }
78
79    /// Constructs a SyncbackSnapshot with the provided path and refs while
80    /// inheriting the data of the this snapshot.
81    #[inline]
82    pub fn with_new_path(&self, path: PathBuf, new_ref: Ref, old_ref: Option<Ref>) -> Self {
83        Self {
84            data: self.data,
85            old: old_ref,
86            new: new_ref,
87            path,
88            middleware: None,
89        }
90    }
91
92    /// Allows a middleware to be 'forced' onto a SyncbackSnapshot to override
93    /// the attempts to derive it.
94    #[inline]
95    pub fn middleware(mut self, middleware: Middleware) -> Self {
96        self.middleware = Some(middleware);
97        self
98    }
99
100    /// Returns a map of properties for an Instance from the 'new' tree
101    /// with filtering done to avoid noise. This method filters out properties
102    /// that are not meant to be present in Instances that are represented
103    /// specially by a path, like `LocalScript.Source` and `StringValue.Value`.
104    ///
105    /// This method is not necessary or desired for blobs like Rbxm or non-path
106    /// middlewares like JsonModel.
107    #[inline]
108    #[must_use]
109    pub fn get_path_filtered_properties(&self, new_ref: Ref) -> Option<UstrMap<&'sync Variant>> {
110        let inst = self.get_new_instance(new_ref)?;
111
112        // The only filtering we have to do is filter out properties that are
113        // special-cased in some capacity.
114        let properties = filter_properties(self.data.project, inst)
115            .into_iter()
116            .filter(|(name, _)| !filter_out_property(inst, name))
117            .collect();
118
119        Some(properties)
120    }
121
122    /// Returns a path to the provided Instance in the new DOM. This path is
123    /// where you would look for the object in Roblox Studio.
124    #[inline]
125    pub fn get_new_inst_path(&self, referent: Ref) -> String {
126        inst_path(self.new_tree(), referent)
127    }
128
129    /// Returns a path to the provided Instance in the old DOM. This path is
130    /// where you would look for the object in Roblox Studio.
131    #[inline]
132    pub fn get_old_inst_path(&self, referent: Ref) -> String {
133        inst_path(self.old_tree(), referent)
134    }
135
136    /// Returns an Instance from the old tree with the provided referent, if it
137    /// exists.
138    #[inline]
139    pub fn get_old_instance(&self, referent: Ref) -> Option<InstanceWithMeta<'sync>> {
140        self.data.old_tree.get_instance(referent)
141    }
142
143    /// Returns an Instance from the new tree with the provided referent, if it
144    /// exists.
145    #[inline]
146    pub fn get_new_instance(&self, referent: Ref) -> Option<&'sync Instance> {
147        self.data.new_tree.get_by_ref(referent)
148    }
149
150    /// The 'old' Instance this snapshot is for, if it exists.
151    #[inline]
152    pub fn old_inst(&self) -> Option<InstanceWithMeta<'sync>> {
153        self.old
154            .and_then(|old| self.data.old_tree.get_instance(old))
155    }
156
157    /// The 'new' Instance this snapshot is for.
158    #[inline]
159    pub fn new_inst(&self) -> &'sync Instance {
160        self.data
161            .new_tree
162            .get_by_ref(self.new)
163            .expect("SyncbackSnapshot should not contain invalid referents")
164    }
165
166    /// Returns the root Project that was used to make this snapshot.
167    #[inline]
168    pub fn project(&self) -> &'sync Project {
169        self.data.project
170    }
171
172    /// Returns the underlying VFS being used for syncback.
173    #[inline]
174    pub fn vfs(&self) -> &'sync Vfs {
175        self.data.vfs
176    }
177
178    /// Returns the WeakDom used for the 'new' tree.
179    #[inline]
180    pub fn new_tree(&self) -> &'sync WeakDom {
181        self.data.new_tree
182    }
183
184    /// Returns the WeakDom used for the 'old' tree.
185    #[inline]
186    pub fn old_tree(&self) -> &'sync WeakDom {
187        self.data.old_tree.inner()
188    }
189
190    /// Returns user-specified property ignore rules.
191    #[inline]
192    pub fn ignore_props(&self) -> Option<&IndexMap<Ustr, Vec<Ustr>>> {
193        self.data
194            .project
195            .syncback_rules
196            .as_ref()
197            .map(|rules| &rules.ignore_properties)
198    }
199
200    /// Returns user-specified ignore tree.
201    #[inline]
202    pub fn ignore_tree(&self) -> Option<&[String]> {
203        self.data
204            .project
205            .syncback_rules
206            .as_ref()
207            .map(|rules| rules.ignore_trees.as_slice())
208    }
209}
210
211pub fn filter_out_property(inst: &Instance, prop_name: &str) -> bool {
212    match inst.class.as_str() {
213        "Script" | "LocalScript" | "ModuleScript" => {
214            // These properties shouldn't be set by scripts that are created via
215            // `$path` or via being on the file system.
216            prop_name == "Source" || prop_name == "ScriptGuid"
217        }
218        "LocalizationTable" => prop_name == "Contents",
219        "StringValue" => prop_name == "Value",
220        _ => false,
221    }
222}
223
224pub fn inst_path(dom: &WeakDom, referent: Ref) -> String {
225    let mut path = Vec::new();
226
227    let mut inst = dom.get_by_ref(referent);
228    while let Some(instance) = inst {
229        path.push(instance.name.as_str());
230        inst = dom.get_by_ref(instance.parent());
231    }
232    // This is to avoid the root's name from appearing in the path. Not
233    // optimal, but should be fine.
234    path.pop();
235
236    path.reverse();
237    path.join("/")
238}
239
240#[cfg(test)]
241mod test {
242    use rbx_dom_weak::{InstanceBuilder, WeakDom};
243
244    use super::inst_path as inst_path_outer;
245
246    #[test]
247    fn inst_path() {
248        let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT"));
249
250        let child_1 = new_tree.insert(new_tree.root_ref(), InstanceBuilder::new("Child1"));
251        let child_2 = new_tree.insert(child_1, InstanceBuilder::new("Child2"));
252        let child_3 = new_tree.insert(child_2, InstanceBuilder::new("Child3"));
253
254        assert_eq!(inst_path_outer(&new_tree, new_tree.root_ref()), "");
255        assert_eq!(inst_path_outer(&new_tree, child_1), "Child1");
256        assert_eq!(inst_path_outer(&new_tree, child_2), "Child1/Child2");
257        assert_eq!(inst_path_outer(&new_tree, child_3), "Child1/Child2/Child3");
258    }
259}