hydrate_data/
diff.rs

1use crate::path_reference::CanonicalPathReference;
2use crate::value::PropertyValue;
3use crate::{
4    AssetId, AssetLocation, AssetName, DataSet, DataSetAssetInfo, DataSetResult, HashSet,
5    NullOverride, OrderedSet, SchemaSet,
6};
7use uuid::Uuid;
8
9#[derive(Debug)]
10pub struct DynamicArrayEntryDelta {
11    key: String,
12    // was previous add/remove, but order is important and this didn't maintain order
13    entries: OrderedSet<Uuid>,
14}
15
16#[derive(Default, Debug)]
17pub struct AssetDiff {
18    set_name: Option<AssetName>,
19    set_location: Option<AssetLocation>,
20    set_prototype: Option<Option<AssetId>>,
21    set_properties: Vec<(String, PropertyValue)>,
22    remove_properties: Vec<String>,
23    set_null_overrides: Vec<(String, NullOverride)>,
24    remove_null_overrides: Vec<String>,
25    add_properties_in_replace_mode: Vec<String>,
26    remove_properties_in_replace_mode: Vec<String>,
27    dynamic_array_entry_deltas: Vec<DynamicArrayEntryDelta>,
28    set_canonical_path_references: Vec<(CanonicalPathReference, AssetId)>,
29    remove_canonical_path_references: Vec<CanonicalPathReference>,
30}
31
32impl AssetDiff {
33    pub fn has_changes(&self) -> bool {
34        self.set_name.is_some()
35            || self.set_location.is_some()
36            || self.set_prototype.is_some()
37            || !self.set_properties.is_empty()
38            || !self.remove_properties.is_empty()
39            || !self.set_null_overrides.is_empty()
40            || !self.remove_null_overrides.is_empty()
41            || !self.add_properties_in_replace_mode.is_empty()
42            || !self.remove_properties_in_replace_mode.is_empty()
43            || !self.dynamic_array_entry_deltas.is_empty()
44            || !self.set_canonical_path_references.is_empty()
45            || !self.remove_canonical_path_references.is_empty()
46    }
47
48    pub fn apply(
49        &self,
50        asset: &mut DataSetAssetInfo,
51    ) {
52        if let Some(set_name) = &self.set_name {
53            asset.asset_name = set_name.clone();
54        }
55
56        if let Some(set_location) = &self.set_location {
57            asset.asset_location = set_location.clone();
58        }
59
60        if let Some(set_prototype) = self.set_prototype {
61            asset.prototype = set_prototype;
62        }
63
64        for (k, v) in &self.set_properties {
65            asset.properties.insert(k.clone(), v.as_value());
66        }
67
68        for k in &self.remove_properties {
69            asset.properties.remove(k);
70        }
71
72        for (k, v) in &self.set_null_overrides {
73            asset.property_null_overrides.insert(k.clone(), *v);
74        }
75
76        for k in &self.remove_null_overrides {
77            asset.property_null_overrides.remove(k);
78        }
79
80        for k in &self.add_properties_in_replace_mode {
81            asset.properties_in_replace_mode.insert(k.clone());
82        }
83
84        for k in &self.remove_properties_in_replace_mode {
85            asset.properties_in_replace_mode.remove(k);
86        }
87
88        for delta in &self.dynamic_array_entry_deltas {
89            if delta.entries.is_empty() {
90                // No entries, just remove the key from the dynamic_collection_entries entirely
91                asset.dynamic_collection_entries.remove(&delta.key);
92            } else {
93                // We have entries, get or create the key, then stomp the value
94                *asset
95                    .dynamic_collection_entries
96                    .entry(delta.key.clone())
97                    .or_default() = delta.entries.clone();
98            }
99        }
100
101        for (k, v) in &self.set_canonical_path_references {
102            asset
103                .build_info
104                .path_reference_overrides
105                .insert(k.clone(), *v);
106        }
107
108        for k in &self.remove_canonical_path_references {
109            asset.build_info.path_reference_overrides.remove(k);
110        }
111    }
112}
113
114pub struct AssetDiffSet {
115    pub apply_diff: AssetDiff,
116    pub revert_diff: AssetDiff,
117}
118
119impl AssetDiffSet {
120    pub fn has_changes(&self) -> bool {
121        // assume if apply has no changes, neither does revert
122        self.apply_diff.has_changes()
123    }
124
125    pub fn diff_assets(
126        before_data_set: &DataSet,
127        before_asset_id: AssetId,
128        after_data_set: &DataSet,
129        after_asset_id: AssetId,
130        modified_locations: &mut HashSet<AssetLocation>,
131    ) -> Self {
132        let before_obj = before_data_set.assets().get(&before_asset_id).unwrap();
133        let after_obj = after_data_set.assets().get(&after_asset_id).unwrap();
134
135        assert_eq!(
136            before_obj.schema().fingerprint(),
137            after_obj.schema().fingerprint()
138        );
139
140        let mut apply_diff = AssetDiff::default();
141        let mut revert_diff = AssetDiff::default();
142
143        if before_obj.asset_name != after_obj.asset_name {
144            apply_diff.set_name = Some(after_obj.asset_name.clone());
145            revert_diff.set_name = Some(before_obj.asset_name.clone());
146        }
147
148        if before_obj.asset_location != after_obj.asset_location {
149            apply_diff.set_location = Some(after_obj.asset_location.clone());
150            revert_diff.set_location = Some(before_obj.asset_location.clone());
151        }
152
153        //
154        // Prototype
155        //
156        if before_obj.prototype != after_obj.prototype {
157            apply_diff.set_prototype = Some(after_obj.prototype);
158            revert_diff.set_prototype = Some(before_obj.prototype);
159        }
160
161        //
162        // Properties
163        //
164        for (key, before_value) in &before_obj.properties {
165            if let Some(after_value) = after_obj.properties.get(key) {
166                if !PropertyValue::are_matching_property_values(before_value, after_value) {
167                    // Value was changed
168                    apply_diff
169                        .set_properties
170                        .push((key.clone(), after_value.as_property_value().unwrap()));
171                    revert_diff
172                        .set_properties
173                        .push((key.clone(), before_value.as_property_value().unwrap()));
174                } else {
175                    // No change
176                }
177            } else {
178                // Property was removed
179                apply_diff.remove_properties.push(key.clone());
180                revert_diff
181                    .set_properties
182                    .push((key.clone(), before_value.as_property_value().unwrap()));
183            }
184        }
185
186        for (key, after_value) in &after_obj.properties {
187            if !before_obj.properties.contains_key(key) {
188                // Property was added
189                apply_diff
190                    .set_properties
191                    .push((key.clone(), after_value.as_property_value().unwrap()));
192                revert_diff.remove_properties.push(key.clone());
193            }
194        }
195
196        //
197        // Null Overrides
198        //
199        for (key, &before_value) in &before_obj.property_null_overrides {
200            if let Some(after_value) = after_obj.property_null_overrides.get(key).copied() {
201                if before_value != after_value {
202                    // Value was changed
203                    apply_diff
204                        .set_null_overrides
205                        .push((key.clone(), after_value));
206                    revert_diff
207                        .set_null_overrides
208                        .push((key.clone(), before_value));
209                } else {
210                    // No change
211                }
212            } else {
213                // Property was removed
214                apply_diff.remove_null_overrides.push(key.clone());
215                revert_diff
216                    .set_null_overrides
217                    .push((key.clone(), before_value));
218            }
219        }
220
221        for (key, &after_value) in &after_obj.property_null_overrides {
222            if !before_obj.property_null_overrides.contains_key(key) {
223                // Property was added
224                apply_diff
225                    .set_null_overrides
226                    .push((key.clone(), after_value));
227                revert_diff.remove_null_overrides.push(key.clone());
228            }
229        }
230
231        //
232        // Properties in replace mode
233        //
234        for replace_mode_property in &before_obj.properties_in_replace_mode {
235            if !after_obj
236                .properties_in_replace_mode
237                .contains(replace_mode_property)
238            {
239                // Replace mode disabled
240                apply_diff
241                    .remove_properties_in_replace_mode
242                    .push(replace_mode_property.clone());
243                revert_diff
244                    .add_properties_in_replace_mode
245                    .push(replace_mode_property.clone());
246            }
247        }
248
249        for replace_mode_property in &after_obj.properties_in_replace_mode {
250            if !before_obj
251                .properties_in_replace_mode
252                .contains(replace_mode_property)
253            {
254                // Replace mode enabled
255                apply_diff
256                    .add_properties_in_replace_mode
257                    .push(replace_mode_property.clone());
258                revert_diff
259                    .remove_properties_in_replace_mode
260                    .push(replace_mode_property.clone());
261            }
262        }
263
264        //
265        // Dynamic Array Entries. THe "key" in this context is a property path.
266        // We do a heavyweight clone so that we can maintain ordering of elements
267        //
268        let mut all_dynamic_entry_keys = HashSet::default();
269        for key in before_obj.dynamic_collection_entries().keys() {
270            all_dynamic_entry_keys.insert(key);
271        }
272        for key in after_obj.dynamic_collection_entries().keys() {
273            all_dynamic_entry_keys.insert(key);
274        }
275        for key in all_dynamic_entry_keys {
276            let empty_set = OrderedSet::<Uuid>::default();
277            let old = before_obj
278                .dynamic_collection_entries
279                .get(key)
280                .unwrap_or(&empty_set);
281            let new = after_obj
282                .dynamic_collection_entries
283                .get(key)
284                .unwrap_or(&empty_set);
285
286            if old != new {
287                apply_diff
288                    .dynamic_array_entry_deltas
289                    .push(DynamicArrayEntryDelta {
290                        key: key.clone(),
291                        entries: new.clone(),
292                    });
293                revert_diff
294                    .dynamic_array_entry_deltas
295                    .push(DynamicArrayEntryDelta {
296                        key: key.clone(),
297                        entries: old.clone(),
298                    });
299            }
300        }
301
302        //
303        // File References
304        //
305        for (key, &before_value) in &before_obj.build_info.path_reference_overrides {
306            if let Some(&after_value) = after_obj.build_info.path_reference_overrides.get(key) {
307                if before_value != after_value {
308                    // Value was changed
309                    apply_diff
310                        .set_canonical_path_references
311                        .push((key.clone(), after_value));
312                    revert_diff
313                        .set_canonical_path_references
314                        .push((key.clone(), before_value));
315                } else {
316                    // No change
317                }
318            } else {
319                // Property was removed
320                apply_diff
321                    .remove_canonical_path_references
322                    .push(key.clone());
323                revert_diff
324                    .set_canonical_path_references
325                    .push((key.clone(), before_value));
326            }
327        }
328
329        for (key, &after_value) in &after_obj.build_info.path_reference_overrides {
330            if !before_obj
331                .build_info
332                .path_reference_overrides
333                .contains_key(key)
334            {
335                // Property was added
336                apply_diff
337                    .set_canonical_path_references
338                    .push((key.clone(), after_value));
339                revert_diff
340                    .remove_canonical_path_references
341                    .push(key.clone());
342            }
343        }
344
345        // we only flag the location as modified if we make an edit
346        // (if apply_diff doesn't have changes, before_diff doesn't either)
347        if apply_diff.has_changes() {
348            if !modified_locations.contains(&after_obj.asset_location) {
349                modified_locations.insert(after_obj.asset_location.clone());
350            }
351
352            // Also save the old location so that in moves, the "from" location is marked as changed too
353            if before_obj.asset_location != after_obj.asset_location {
354                if !modified_locations.contains(&before_obj.asset_location) {
355                    modified_locations.insert(before_obj.asset_location.clone());
356                }
357            }
358        }
359
360        AssetDiffSet {
361            apply_diff,
362            revert_diff,
363        }
364    }
365}
366
367#[derive(Default, Debug)]
368pub struct DataSetDiff {
369    creates: Vec<(AssetId, DataSetAssetInfo)>,
370    deletes: Vec<AssetId>,
371    changes: Vec<(AssetId, AssetDiff)>,
372}
373
374impl DataSetDiff {
375    pub fn has_changes(&self) -> bool {
376        !self.creates.is_empty() || !self.deletes.is_empty() || !self.changes.is_empty()
377    }
378
379    pub fn apply(
380        &self,
381        data_set: &mut DataSet,
382        schema_set: &SchemaSet,
383    ) -> DataSetResult<()> {
384        for delete in &self.deletes {
385            data_set.delete_asset(*delete)?;
386        }
387
388        for (id, create) in &self.creates {
389            data_set.restore_asset(
390                *id,
391                create.asset_name.clone(),
392                create.asset_location.clone(),
393                create.import_info.clone(),
394                create.build_info.clone(),
395                schema_set,
396                create.prototype,
397                create.schema().fingerprint(),
398                create.properties.clone(),
399                create.property_null_overrides.clone(),
400                create.properties_in_replace_mode.clone(),
401                create.dynamic_collection_entries.clone(),
402            )?;
403        }
404
405        for (asset_id, v) in &self.changes {
406            if let Some(asset) = data_set.assets_mut().get_mut(asset_id) {
407                v.apply(asset);
408            }
409        }
410
411        Ok(())
412    }
413
414    pub fn get_modified_assets(
415        &self,
416        modified_assets: &mut HashSet<AssetId>,
417    ) {
418        for (id, _) in &self.creates {
419            modified_assets.insert(*id);
420        }
421
422        for id in &self.deletes {
423            modified_assets.insert(*id);
424        }
425
426        for (id, _) in &self.changes {
427            modified_assets.insert(*id);
428        }
429    }
430}
431
432#[derive(Debug)]
433pub struct DataSetDiffSet {
434    pub apply_diff: DataSetDiff,
435    pub revert_diff: DataSetDiff,
436    pub modified_assets: HashSet<AssetId>,
437    pub modified_locations: HashSet<AssetLocation>,
438}
439
440impl DataSetDiffSet {
441    pub fn has_changes(&self) -> bool {
442        // assume if apply has no changes, neither does revert
443        self.apply_diff.has_changes()
444    }
445
446    pub fn diff_data_set(
447        before: &DataSet,
448        after: &DataSet,
449        tracked_assets: &HashSet<AssetId>,
450    ) -> Self {
451        let mut apply_diff = DataSetDiff::default();
452        let mut revert_diff = DataSetDiff::default();
453        let mut modified_assets: HashSet<AssetId> = Default::default();
454        let mut modified_locations: HashSet<AssetLocation> = Default::default();
455
456        // Check for created assets
457        for &asset_id in tracked_assets {
458            let existed_before = before.assets().contains_key(&asset_id);
459            let existed_after = after.assets().contains_key(&asset_id);
460            if existed_before {
461                if existed_after {
462                    // Asset was modified
463                    let diff = AssetDiffSet::diff_assets(
464                        before,
465                        asset_id,
466                        &after,
467                        asset_id,
468                        &mut modified_locations,
469                    );
470                    if diff.has_changes() {
471                        modified_assets.insert(asset_id);
472                        apply_diff.changes.push((asset_id, diff.apply_diff));
473                        revert_diff.changes.push((asset_id, diff.revert_diff));
474                    }
475                } else {
476                    // Asset was deleted
477                    let before_asset_info = before.assets().get(&asset_id).unwrap().clone();
478                    modified_assets.insert(asset_id);
479                    if !modified_locations.contains(&before_asset_info.asset_location) {
480                        modified_locations.insert(before_asset_info.asset_location.clone());
481                    }
482
483                    // deleted
484                    apply_diff.deletes.push(asset_id);
485                    revert_diff
486                        .creates
487                        .push((asset_id, before_asset_info.clone()));
488                }
489            } else if existed_after {
490                // Asset was created
491                let after_asset_info = after.assets().get(&asset_id).unwrap();
492                if !modified_locations.contains(&after_asset_info.asset_location) {
493                    modified_locations.insert(after_asset_info.asset_location.clone());
494                }
495
496                // created
497                apply_diff
498                    .creates
499                    .push((asset_id, after_asset_info.clone()));
500                revert_diff.deletes.push(asset_id);
501            }
502        }
503
504        DataSetDiffSet {
505            apply_diff,
506            revert_diff,
507            modified_locations,
508            modified_assets,
509        }
510    }
511}