hydrate_data/
data_set.rs

1use crate::path_reference::CanonicalPathReference;
2use crate::{
3    AssetId, HashMap, HashSet, OrderedSet, PathReference, PathReferenceHash, Schema,
4    SchemaFingerprint, SchemaRecord, SingleObject, Value,
5};
6pub use crate::{DataSetError, DataSetResult};
7use crate::{NullOverride, SchemaSet};
8use siphasher::sip::SipHasher;
9use std::cmp::Ordering;
10use std::hash::{Hash, Hasher};
11use std::str::FromStr;
12use std::string::ToString;
13use uuid::Uuid;
14
15#[derive(Copy, Clone, PartialEq)]
16pub enum HashObjectMode {
17    // Used for detecting change in the asset that affects build output:
18    // - Detection if the build is stale by comparing hash of all objects to previous completed build manifest hash
19    // - Knowing what state an object was in when it was read by a build job
20    //
21    // This mode looks at properties and prototype properties, even when they are in different data sources
22    PropertiesOnly,
23
24    // These are used to know if an asset matches the state it was in from storage. It does not look at the prototype
25    // chain because prototypes are in different files
26    FullObjectWithLocationId,
27    FullObjectWithLocationChainNames,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd)]
31pub struct AssetName(String);
32
33impl AssetName {
34    pub fn new<T: Into<String>>(name: T) -> Self {
35        AssetName(name.into())
36    }
37
38    pub fn empty() -> Self {
39        AssetName(String::default())
40    }
41
42    pub fn is_empty(&self) -> bool {
43        self.0.is_empty()
44    }
45
46    pub fn as_string(&self) -> Option<&String> {
47        if self.0.is_empty() {
48            None
49        } else {
50            Some(&self.0)
51        }
52    }
53}
54
55impl Ord for AssetName {
56    fn cmp(
57        &self,
58        other: &Self,
59    ) -> Ordering {
60        self.0.to_lowercase().cmp(&other.0.to_lowercase())
61    }
62}
63
64#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
65pub struct AssetLocation {
66    path_node_id: AssetId,
67}
68
69impl AssetLocation {
70    pub fn new(path_node_id: AssetId) -> Self {
71        AssetLocation { path_node_id }
72    }
73
74    pub fn null() -> AssetLocation {
75        AssetLocation {
76            path_node_id: AssetId::null(),
77        }
78    }
79
80    pub fn path_node_id(&self) -> AssetId {
81        self.path_node_id
82    }
83
84    pub fn is_null(&self) -> bool {
85        self.path_node_id.is_null()
86    }
87}
88
89#[derive(Debug, Copy, Clone, PartialEq)]
90pub enum OverrideBehavior {
91    Append,
92    Replace,
93}
94
95#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
96pub struct ImporterId(pub Uuid);
97
98#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
99pub struct BuilderId(pub usize);
100
101// This newtype ensures we do not allow both a None and a Some("") importable name
102#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
103pub struct ImportableName(String);
104
105impl ImportableName {
106    // In some cases in serialization it is convenient to let empty string imply a None importable
107    // name. This constructor makes this convenient.
108    pub fn new(name: String) -> Self {
109        ImportableName(name)
110    }
111
112    // Does not accept Some("") as this is ambiguous between a None name and an empty string name.
113    // "" is not an allowed importable name.
114    pub fn new_optional(name: Option<String>) -> Self {
115        if let Some(name) = name {
116            assert!(!name.is_empty());
117            ImportableName(name)
118        } else {
119            ImportableName(String::default())
120        }
121    }
122
123    pub fn name(&self) -> Option<&str> {
124        if self.0.is_empty() {
125            None
126        } else {
127            Some(&self.0)
128        }
129    }
130
131    pub fn is_default(&self) -> bool {
132        self.0.is_empty()
133    }
134}
135
136/// Describes the conditions that we imported the file
137#[derive(Clone, Debug)]
138pub struct ImportInfo {
139    // Set on initial import
140    importer_id: ImporterId,
141
142    // Set on initial import, or re-import. This affects the import step.
143    // Anything that just affects the build step should be an asset property instead.
144    // Removed for now as I don't have a practical use for it right now. Generally I think we could
145    // lean towards importing everything and using asset properties to make the build step produce
146    // less data if we don't want everything.
147    //import_options: SingleObject,
148
149    // Set on initial import, or re-import. Used to monitor to detect stale imported data and
150    // automaticlaly re-import, and as a heuristic when importing other files that reference this
151    // file to link to this asset rather than importing another copy.
152    source_file: CanonicalPathReference,
153
154    // All the file references that need to be resolved in order to build the asset (this represents
155    // file references encountered in the input data, and only changes when data is re-imported)
156    path_references: HashMap<PathReferenceHash, CanonicalPathReference>,
157
158    // State of the source file when the asset was imported
159    source_file_modified_timestamp: u64,
160    source_file_size: u64,
161
162    // Hash of the import data
163    import_data_contents_hash: u64,
164}
165
166impl ImportInfo {
167    pub fn new(
168        importer_id: ImporterId,
169        source_file: CanonicalPathReference,
170        path_references: HashMap<PathReferenceHash, CanonicalPathReference>,
171        source_file_modified_timestamp: u64,
172        source_file_size: u64,
173        import_data_contents_hash: u64,
174    ) -> Self {
175        ImportInfo {
176            importer_id,
177            source_file,
178            path_references,
179            source_file_modified_timestamp,
180            source_file_size,
181            import_data_contents_hash,
182        }
183    }
184
185    pub fn importer_id(&self) -> ImporterId {
186        self.importer_id
187    }
188
189    pub fn source_file(&self) -> &CanonicalPathReference {
190        &self.source_file
191    }
192
193    pub fn importable_name(&self) -> &ImportableName {
194        self.source_file.importable_name()
195    }
196
197    pub fn path_references(&self) -> &HashMap<PathReferenceHash, CanonicalPathReference> {
198        &self.path_references
199    }
200
201    pub fn source_file_modified_timestamp(&self) -> u64 {
202        self.source_file_modified_timestamp
203    }
204
205    pub fn source_file_size(&self) -> u64 {
206        self.source_file_size
207    }
208
209    pub fn import_data_contents_hash(&self) -> u64 {
210        self.import_data_contents_hash
211    }
212}
213
214impl Hash for ImportInfo {
215    fn hash<H: Hasher>(
216        &self,
217        state: &mut H,
218    ) {
219        self.importer_id.hash(state);
220        self.source_file.hash(state);
221
222        let mut path_references_hashes = 0u64;
223        for (k, v) in &self.path_references {
224            let mut inner_hasher = SipHasher::new();
225            k.hash(&mut inner_hasher);
226            v.hash(&mut inner_hasher);
227            path_references_hashes = path_references_hashes ^ inner_hasher.finish();
228        }
229
230        self.source_file_modified_timestamp.hash(state);
231        self.source_file_size.hash(state);
232        self.import_data_contents_hash.hash(state);
233    }
234}
235
236/// Affects how we build the file. However most of the time use asset properties instead. The only
237/// things in here should be system-level configuration that is relevant to any asset type
238#[derive(Clone, Debug, Default)]
239pub struct BuildInfo {
240    // Imported files often reference other files. During import, referenced files will also be
241    // imported. We maintain the correlation between paths and imported asset ID here for use when
242    // processing the imported data.
243    pub path_reference_overrides: HashMap<CanonicalPathReference, AssetId>,
244}
245
246impl Hash for BuildInfo {
247    fn hash<H: Hasher>(
248        &self,
249        state: &mut H,
250    ) {
251        let mut path_references_overrides_hashes = 0u64;
252        for (k, v) in &self.path_reference_overrides {
253            let mut inner_hasher = SipHasher::new();
254            k.hash(&mut inner_hasher);
255            v.hash(&mut inner_hasher);
256            path_references_overrides_hashes =
257                path_references_overrides_hashes ^ inner_hasher.finish();
258        }
259
260        path_references_overrides_hashes.hash(state);
261    }
262}
263
264// Allows for copying data from one place and applying it elsewhere
265pub struct PropertiesBundle {
266    schema: Schema,
267    properties: HashMap<String, Value>,
268    property_null_overrides: HashMap<String, NullOverride>,
269    properties_in_replace_mode: HashSet<String>,
270    dynamic_collection_entries: HashMap<String, OrderedSet<Uuid>>,
271}
272
273impl PropertiesBundle {
274    fn read(
275        asset_info: &DataSetAssetInfo,
276        path_prefix: impl AsRef<str>,
277        schema_set: &SchemaSet,
278    ) -> DataSetResult<PropertiesBundle> {
279        let path_prefix_str = path_prefix.as_ref();
280        let prefix_string = if path_prefix_str.is_empty() {
281            Default::default()
282        } else {
283            format!("{}", path_prefix_str)
284        };
285
286        let schema = asset_info
287            .schema()
288            .find_property_schema(path_prefix_str, schema_set.schemas())
289            .ok_or(DataSetError::SchemaNotFound)?;
290
291        let mut properties = HashMap::<String, Value>::default();
292        println!("Look for property {:?}", path_prefix_str);
293        for (k, v) in &asset_info.properties {
294            println!("    property {:?}", k);
295            if k.starts_with(&prefix_string) {
296                properties.insert(k[prefix_string.len()..].to_string(), v.clone());
297            }
298        }
299
300        let mut property_null_overrides = HashMap::<String, NullOverride>::default();
301        for (k, v) in &asset_info.property_null_overrides {
302            if k.starts_with(&prefix_string) {
303                property_null_overrides.insert(k[prefix_string.len()..].to_string(), v.clone());
304            }
305        }
306
307        let mut properties_in_replace_mode = HashSet::<String>::default();
308        for k in &asset_info.properties_in_replace_mode {
309            if k.starts_with(&prefix_string) {
310                properties_in_replace_mode.insert(k[prefix_string.len()..].to_string());
311            }
312        }
313
314        let mut dynamic_collection_entries = HashMap::<String, OrderedSet<Uuid>>::default();
315        for (k, v) in &asset_info.dynamic_collection_entries {
316            if k.starts_with(&prefix_string) {
317                dynamic_collection_entries.insert(k[prefix_string.len()..].to_string(), v.clone());
318            }
319        }
320
321        Ok(PropertiesBundle {
322            schema,
323            properties,
324            property_null_overrides,
325            properties_in_replace_mode,
326            dynamic_collection_entries,
327        })
328    }
329
330    fn write(
331        &self,
332        asset_info: &mut DataSetAssetInfo,
333        path_prefix: impl AsRef<str>,
334        schema_set: &SchemaSet,
335    ) -> DataSetResult<()> {
336        let path_prefix_str = path_prefix.as_ref();
337        let prefix_string = if path_prefix_str.is_empty() {
338            Default::default()
339        } else {
340            format!("{}", path_prefix_str)
341        };
342
343        //
344        // verify schema match at dest prefix
345        //
346        let schema = asset_info
347            .schema()
348            .find_property_schema(path_prefix_str, schema_set.schemas())
349            .ok_or(DataSetError::SchemaNotFound)?;
350        assert_eq!(schema, self.schema);
351
352        //
353        // wipe anything that was there
354        //
355        asset_info
356            .properties
357            .retain(|k, _| !k.starts_with(&prefix_string));
358        asset_info
359            .property_null_overrides
360            .retain(|k, _| !k.starts_with(&prefix_string));
361        asset_info
362            .properties_in_replace_mode
363            .retain(|k| !k.starts_with(&prefix_string));
364        asset_info
365            .dynamic_collection_entries
366            .retain(|k, _| !k.starts_with(&prefix_string));
367
368        //
369        // stomp with new data
370        //
371        for (k, v) in &self.properties {
372            asset_info
373                .properties
374                .insert(format!("{}{}", prefix_string, k), v.clone());
375        }
376
377        for (k, v) in &self.property_null_overrides {
378            asset_info
379                .property_null_overrides
380                .insert(format!("{}{}", prefix_string, k), v.clone());
381        }
382
383        for k in &self.properties_in_replace_mode {
384            asset_info
385                .properties_in_replace_mode
386                .insert(format!("{}{}", prefix_string, k));
387        }
388
389        for (k, v) in &self.dynamic_collection_entries {
390            asset_info
391                .dynamic_collection_entries
392                .insert(format!("{}{}", prefix_string, k), v.clone());
393        }
394
395        Ok(())
396    }
397}
398
399/// The full state of a single asset in a dataset
400#[derive(Clone, Debug)]
401pub struct DataSetAssetInfo {
402    schema: SchemaRecord,
403
404    pub(super) asset_name: AssetName,
405    pub(super) asset_location: AssetLocation,
406
407    // Stores the configuration/choices that were made when the asset was last imported
408    pub(super) import_info: Option<ImportInfo>,
409    pub(super) build_info: BuildInfo,
410
411    pub(super) prototype: Option<AssetId>,
412    pub(super) properties: HashMap<String, Value>,
413    pub(super) property_null_overrides: HashMap<String, NullOverride>,
414    pub(super) properties_in_replace_mode: HashSet<String>,
415    pub(super) dynamic_collection_entries: HashMap<String, OrderedSet<Uuid>>,
416}
417
418impl DataSetAssetInfo {
419    pub fn schema(&self) -> &SchemaRecord {
420        &self.schema
421    }
422
423    pub fn asset_name(&self) -> &AssetName {
424        &self.asset_name
425    }
426
427    pub fn asset_location(&self) -> AssetLocation {
428        self.asset_location
429    }
430
431    pub fn import_info(&self) -> &Option<ImportInfo> {
432        &self.import_info
433    }
434
435    pub fn build_info(&self) -> &BuildInfo {
436        &self.build_info
437    }
438
439    pub fn prototype(&self) -> Option<AssetId> {
440        self.prototype
441    }
442
443    pub fn properties(&self) -> &HashMap<String, Value> {
444        &self.properties
445    }
446
447    pub fn property_null_overrides(&self) -> &HashMap<String, NullOverride> {
448        &self.property_null_overrides
449    }
450
451    pub fn properties_in_replace_mode(&self) -> &HashSet<String> {
452        &self.properties_in_replace_mode
453    }
454
455    pub fn dynamic_collection_entries(&self) -> &HashMap<String, OrderedSet<Uuid>> {
456        &self.dynamic_collection_entries
457    }
458}
459
460/// A collection of assets. Methods support serializing/deserializing, resolving property values,
461/// etc. This includes being aware of schema and prototypes.
462#[derive(Default, Clone)]
463pub struct DataSet {
464    assets: HashMap<AssetId, DataSetAssetInfo>,
465}
466
467impl DataSet {
468    pub fn assets(&self) -> &HashMap<AssetId, DataSetAssetInfo> {
469        &self.assets
470    }
471
472    // Exposed to allow diffs to apply changes
473    pub(super) fn assets_mut(&mut self) -> &mut HashMap<AssetId, DataSetAssetInfo> {
474        &mut self.assets
475    }
476
477    pub fn take_assets(self) -> HashMap<AssetId, DataSetAssetInfo> {
478        self.assets
479    }
480
481    // Inserts the asset but only if the ID is not already in use
482    fn insert_asset(
483        &mut self,
484        id: AssetId,
485        obj_info: DataSetAssetInfo,
486    ) -> DataSetResult<()> {
487        if self.assets.contains_key(&id) {
488            Err(DataSetError::DuplicateAssetId)?
489        } else {
490            let old = self.assets.insert(id, obj_info);
491            assert!(old.is_none());
492            Ok(())
493        }
494    }
495
496    /// Creates the asset, overwriting it if it already exists
497    pub fn restore_asset(
498        &mut self,
499        asset_id: AssetId,
500        asset_name: AssetName,
501        asset_location: AssetLocation,
502        import_info: Option<ImportInfo>,
503        build_info: BuildInfo,
504        schema_set: &SchemaSet,
505        prototype: Option<AssetId>,
506        schema: SchemaFingerprint,
507        properties: HashMap<String, Value>,
508        property_null_overrides: HashMap<String, NullOverride>,
509        properties_in_replace_mode: HashSet<String>,
510        dynamic_collection_entries: HashMap<String, OrderedSet<Uuid>>,
511    ) -> DataSetResult<()> {
512        let schema = schema_set
513            .schemas()
514            .get(&schema)
515            .ok_or(DataSetError::SchemaNotFound)?;
516        let schema_record = schema.as_record().cloned()?;
517        let obj = DataSetAssetInfo {
518            schema: schema_record,
519            asset_name,
520            asset_location,
521            import_info,
522            build_info,
523            prototype,
524            properties,
525            property_null_overrides,
526            properties_in_replace_mode,
527            dynamic_collection_entries,
528        };
529
530        self.assets.insert(asset_id, obj);
531        Ok(())
532    }
533
534    /// Creates an asset with a particular ID with no properties set. Fails if the asset ID is already
535    /// in use.
536    pub fn new_asset_with_id(
537        &mut self,
538        asset_id: AssetId,
539        asset_name: AssetName,
540        asset_location: AssetLocation,
541        schema: &SchemaRecord,
542    ) -> DataSetResult<()> {
543        let obj = DataSetAssetInfo {
544            schema: schema.clone(),
545            asset_name: asset_name,
546            asset_location: asset_location,
547            import_info: None,
548            build_info: Default::default(),
549            prototype: None,
550            properties: Default::default(),
551            property_null_overrides: Default::default(),
552            properties_in_replace_mode: Default::default(),
553            dynamic_collection_entries: Default::default(),
554        };
555
556        self.insert_asset(asset_id, obj)
557    }
558
559    /// Creates a new asset with no properties set. Uses a unique UUID and should not fail
560    pub fn new_asset(
561        &mut self,
562        asset_name: AssetName,
563        asset_location: AssetLocation,
564        schema: &SchemaRecord,
565    ) -> AssetId {
566        let id = AssetId::from_uuid(Uuid::new_v4());
567
568        // The unwrap here is safe because a duplicate UUID is statistically very unlikely
569        self.new_asset_with_id(id, asset_name, asset_location, schema)
570            .expect("Randomly created UUID collided with existing UUID");
571
572        id
573    }
574
575    /// Creates a new asset and sets it to use the given prototype asset ID as the new object's prototype
576    /// May fail if the prototype asset is not found
577    pub fn new_asset_from_prototype(
578        &mut self,
579        asset_name: AssetName,
580        asset_location: AssetLocation,
581        prototype_asset_id: AssetId,
582    ) -> DataSetResult<AssetId> {
583        let prototype_schema = self
584            .assets
585            .get(&prototype_asset_id)
586            .ok_or(DataSetError::AssetNotFound)?;
587
588        let id = self.new_asset(
589            asset_name,
590            asset_location,
591            &prototype_schema.schema().clone(),
592        );
593        self.assets
594            .get_mut(&id)
595            .expect("Newly created asset was not found")
596            .prototype = Some(prototype_asset_id);
597        Ok(id)
598    }
599
600    /// Populate an empty asset with data from a SingleObject. The asset should already exist, and
601    /// the schema must match.
602    pub fn copy_from_single_object(
603        &mut self,
604        asset_id: AssetId,
605        single_object: &SingleObject,
606    ) -> DataSetResult<()> {
607        let asset = self
608            .assets
609            .get_mut(&asset_id)
610            .ok_or(DataSetError::AssetNotFound)?;
611
612        if asset.schema.fingerprint() != single_object.schema().fingerprint() {
613            return Err(DataSetError::SingleObjectDoesNotMatchSchema)?;
614        };
615
616        // Reset the state
617        asset.prototype = None;
618        asset.properties.clear();
619        asset.property_null_overrides.clear();
620        asset.properties_in_replace_mode.clear();
621        asset.dynamic_collection_entries.clear();
622
623        for (property, value) in single_object.properties() {
624            asset.properties.insert(property.clone(), value.clone());
625        }
626
627        for (property, null_override) in single_object.property_null_overrides() {
628            asset
629                .property_null_overrides
630                .insert(property.clone(), *null_override);
631        }
632
633        for (property, dynamic_collection_entries) in single_object.dynamic_collection_entries() {
634            let property_entry = asset
635                .dynamic_collection_entries
636                .entry(property.clone())
637                .or_default();
638            for element in &*dynamic_collection_entries {
639                let is_newly_inserted = property_entry.try_insert_at_end(*element);
640                // elements are UUIDs and they should have been unique
641                assert!(is_newly_inserted);
642            }
643        }
644
645        Ok(())
646    }
647
648    pub fn duplicate_asset(
649        &mut self,
650        asset_id: AssetId,
651        schema_set: &SchemaSet,
652    ) -> DataSetResult<AssetId> {
653        let old_asset = self
654            .assets
655            .get(&asset_id)
656            .ok_or(DataSetError::AssetNotFound)?;
657
658        let new_asset_id = AssetId(Uuid::new_v4());
659        let mut name_count = 1;
660        let new_name = loop {
661            let new_name = if name_count == 1 {
662                AssetName::new(format!("Copy of {}", old_asset.asset_name.0))
663            } else {
664                AssetName::new(format!("Copy of {} {}", old_asset.asset_name.0, name_count))
665            };
666
667            let mut is_duplicate = false;
668            for (_, asset_info) in &self.assets {
669                if asset_info.asset_name == new_name
670                    && asset_info.asset_location == old_asset.asset_location
671                {
672                    is_duplicate = true;
673                    break;
674                }
675            }
676
677            if !is_duplicate {
678                break new_name;
679            }
680
681            name_count += 1;
682        };
683
684        self.restore_asset(
685            new_asset_id,
686            new_name,
687            old_asset.asset_location,
688            old_asset.import_info.clone(),
689            old_asset.build_info.clone(),
690            schema_set,
691            old_asset.prototype,
692            old_asset.schema.fingerprint(),
693            old_asset.properties.clone(),
694            old_asset.property_null_overrides.clone(),
695            old_asset.properties_in_replace_mode.clone(),
696            old_asset.dynamic_collection_entries.clone(),
697        )?;
698        Ok(new_asset_id)
699    }
700
701    /// Returns error if asset did not exist
702    pub fn delete_asset(
703        &mut self,
704        asset_id: AssetId,
705    ) -> DataSetResult<()> {
706        if self.assets.remove(&asset_id).is_none() {
707            Err(DataSetError::AssetNotFound)?
708        } else {
709            Ok(())
710        }
711    }
712
713    /// Returns error if asset does not exist
714    pub fn set_asset_location(
715        &mut self,
716        asset_id: AssetId,
717        new_location: AssetLocation,
718    ) -> DataSetResult<()> {
719        let mut new_parent_asset_id_iter = Some(new_location.path_node_id());
720        while let Some(new_parent_asset_id) = new_parent_asset_id_iter {
721            if new_parent_asset_id == asset_id {
722                // Cannot make an asset a child of its own children
723                return Err(DataSetError::NewLocationIsChildOfCurrentAsset)?;
724            }
725            new_parent_asset_id_iter = self
726                .asset_location(new_parent_asset_id)
727                .map(|x| x.path_node_id())
728        }
729
730        let asset = self
731            .assets
732            .get_mut(&asset_id)
733            .ok_or(DataSetError::AssetNotFound)?;
734
735        asset.asset_location = new_location;
736        Ok(())
737    }
738
739    /// Returns error if asset does not exist
740    pub fn set_import_info(
741        &mut self,
742        asset_id: AssetId,
743        import_info: ImportInfo,
744    ) -> DataSetResult<()> {
745        let asset = self
746            .assets
747            .get_mut(&asset_id)
748            .ok_or(DataSetError::AssetNotFound)?;
749
750        asset.import_info = Some(import_info);
751        Ok(())
752    }
753
754    /// Returns error if other asset does not exist. This will create or overwrite the asset in this
755    /// dataset and does not require that the schema be the same if the asset already existed. No
756    /// validation is performed to ensure that references to other assets or the prototype exist.
757    pub fn copy_from(
758        &mut self,
759        other: &DataSet,
760        asset_id: AssetId,
761    ) -> DataSetResult<()> {
762        let asset = other
763            .assets
764            .get(&asset_id)
765            .ok_or(DataSetError::AssetNotFound)?;
766
767        self.assets.insert(asset_id, asset.clone());
768        Ok(())
769    }
770
771    /// Returns the asset name, or none if the asset was not found
772    pub fn asset_name(
773        &self,
774        asset_id: AssetId,
775    ) -> DataSetResult<&AssetName> {
776        Ok(self
777            .assets
778            .get(&asset_id)
779            .ok_or(DataSetError::AssetNotFound)?
780            .asset_name())
781    }
782
783    /// Sets the asset's name, fails if the asset does not exist
784    pub fn set_asset_name(
785        &mut self,
786        asset_id: AssetId,
787        asset_name: AssetName,
788    ) -> DataSetResult<()> {
789        let asset = self
790            .assets
791            .get_mut(&asset_id)
792            .ok_or(DataSetError::AssetNotFound)?;
793
794        asset.asset_name = asset_name;
795        Ok(())
796    }
797
798    /// Returns the asset's parent or none if the asset does not exist
799    pub fn asset_location(
800        &self,
801        asset_id: AssetId,
802    ) -> Option<AssetLocation> {
803        self.assets
804            .get(&asset_id)
805            .map(|x| &x.asset_location)
806            .copied()
807    }
808
809    /// Returns the asset locations from the parent all the way up to the root parent. If a cycle is
810    /// detected or any elements in the chain are not found, an error is returned
811    pub fn asset_location_chain(
812        &self,
813        asset_id: AssetId,
814    ) -> DataSetResult<Vec<AssetLocation>> {
815        let mut asset_location_chain = Vec::default();
816
817        // If this asset's location is none, return an empty list
818        let Some(mut obj_iter) = self.asset_location(asset_id) else {
819            return Ok(asset_location_chain);
820        };
821
822        // Iterate up the chain
823        while !obj_iter.path_node_id.is_null() {
824            if asset_location_chain.contains(&obj_iter) {
825                // Detected a cycle, return an empty list
826                return Err(DataSetError::LocationCycleDetected)?;
827            }
828
829            asset_location_chain.push(obj_iter.clone());
830            obj_iter = if let Some(location) = self.asset_location(obj_iter.path_node_id) {
831                // May be null, in which case we will terminate and return this list so far not including the null
832                location
833            } else {
834                // The parent was specified but not found, default to empty list if the chain is in a bad state
835                return Err(DataSetError::LocationParentNotFound)?;
836            };
837        }
838
839        Ok(asset_location_chain)
840    }
841
842    /// Gets the import info, returns None if the asset does not exist or there is no import info
843    /// associated with the asset
844    pub fn import_info(
845        &self,
846        asset_id: AssetId,
847    ) -> Option<&ImportInfo> {
848        self.assets
849            .get(&asset_id)
850            .map(|x| x.import_info.as_ref())
851            .flatten()
852    }
853
854    fn do_resolve_path_reference_into_canonical_path_reference<'a>(
855        &'a self,
856        asset: &'a DataSetAssetInfo,
857        path_reference_hash: PathReferenceHash,
858    ) -> Option<&'a CanonicalPathReference> {
859        // Can we find the canonical path in our import info?
860        if let Some(import_info) = &asset.import_info {
861            if let Some(canonical_path) = import_info.path_references.get(&path_reference_hash) {
862                return Some(canonical_path);
863            }
864        }
865
866        // Otherwise follow our prototype chain
867        if let Some(prototype) = asset.prototype {
868            // Silently ignore a missing prototype, we treat broken prototype references as acting like there was
869            // no prototype reference
870            if let Some(prototype_asset) = self.assets.get(&prototype) {
871                return self.do_resolve_path_reference_into_canonical_path_reference(
872                    prototype_asset,
873                    path_reference_hash,
874                );
875            }
876        }
877
878        None
879    }
880
881    fn do_resolve_canonical_path_reference_into_asset_id(
882        &self,
883        asset: &DataSetAssetInfo,
884        canonical_path: &CanonicalPathReference,
885    ) -> Option<AssetId> {
886        // Can we find the asset id in our build info?
887        if let Some(referenced_asset_id) = asset
888            .build_info
889            .path_reference_overrides
890            .get(canonical_path)
891        {
892            return Some(*referenced_asset_id);
893        }
894
895        // Otherwise follow our prototype chain
896        if let Some(prototype) = asset.prototype {
897            // Silently ignore a missing prototype, we treat broken prototype references as acting like there was
898            // no prototype reference
899            if let Some(prototype_asset) = self.assets.get(&prototype) {
900                return self.do_resolve_canonical_path_reference_into_asset_id(
901                    prototype_asset,
902                    canonical_path,
903                );
904            }
905        }
906
907        None
908    }
909
910    fn do_resolve_all_path_references_into_canonical_path_references(
911        &self,
912        asset: &DataSetAssetInfo,
913        all_references: &mut HashMap<PathReferenceHash, CanonicalPathReference>,
914    ) -> DataSetResult<()> {
915        if let Some(prototype) = asset.prototype {
916            // Silently ignore a missing prototype, we treat broken prototype references as acting like there was
917            // no prototype reference
918            if let Some(prototype_asset) = self.assets.get(&prototype) {
919                self.do_resolve_all_path_references_into_canonical_path_references(
920                    prototype_asset,
921                    all_references,
922                )?;
923            }
924        }
925
926        if let Some(import_info) = &asset.import_info {
927            for (k, v) in &import_info.path_references {
928                all_references.insert(*k, v.clone());
929            }
930        }
931
932        Ok(())
933    }
934
935    fn do_resolve_all_canonical_path_references_into_asset_id(
936        &self,
937        asset: &DataSetAssetInfo,
938        all_references: &mut HashMap<CanonicalPathReference, AssetId>,
939    ) -> DataSetResult<()> {
940        if let Some(prototype) = asset.prototype {
941            // Silently ignore a missing prototype, we treat broken prototype references as acting like there was
942            // no prototype reference
943            if let Some(prototype_asset) = self.assets.get(&prototype) {
944                self.do_resolve_all_canonical_path_references_into_asset_id(
945                    prototype_asset,
946                    all_references,
947                )?;
948            }
949        }
950
951        for (k, v) in &asset.build_info.path_reference_overrides {
952            all_references.insert(k.clone(), *v);
953        }
954
955        Ok(())
956    }
957
958    pub fn resolve_path_reference<P: Into<PathReference>>(
959        &self,
960        asset_id: AssetId,
961        path: P,
962    ) -> DataSetResult<Option<AssetId>> {
963        let asset = self
964            .assets
965            .get(&asset_id)
966            .ok_or(DataSetError::AssetNotFound)?;
967        let canonical_path = self.do_resolve_path_reference_into_canonical_path_reference(
968            asset,
969            path.into().path_reference_hash(),
970        );
971        Ok(if let Some(canonical_path) = canonical_path {
972            self.do_resolve_canonical_path_reference_into_asset_id(asset, canonical_path)
973        } else {
974            None
975        })
976    }
977
978    pub fn resolve_canonical_path_reference(
979        &self,
980        asset_id: AssetId,
981        canonical_path: &CanonicalPathReference,
982    ) -> DataSetResult<Option<AssetId>> {
983        let asset = self
984            .assets
985            .get(&asset_id)
986            .ok_or(DataSetError::AssetNotFound)?;
987        Ok(self.do_resolve_canonical_path_reference_into_asset_id(asset, canonical_path))
988    }
989
990    pub fn resolve_all_hashed_path_references(
991        &self,
992        asset_id: AssetId,
993    ) -> DataSetResult<HashMap<PathReferenceHash, CanonicalPathReference>> {
994        let asset = self
995            .assets
996            .get(&asset_id)
997            .ok_or(DataSetError::AssetNotFound)?;
998        let mut all_references = HashMap::default();
999        self.do_resolve_all_path_references_into_canonical_path_references(
1000            asset,
1001            &mut all_references,
1002        )?;
1003        Ok(all_references)
1004    }
1005
1006    pub fn resolve_all_path_reference_overrides(
1007        &self,
1008        asset_id: AssetId,
1009    ) -> DataSetResult<HashMap<CanonicalPathReference, AssetId>> {
1010        let asset = self
1011            .assets
1012            .get(&asset_id)
1013            .ok_or(DataSetError::AssetNotFound)?;
1014        let mut all_references = HashMap::default();
1015        self.do_resolve_all_canonical_path_references_into_asset_id(asset, &mut all_references)?;
1016        Ok(all_references)
1017    }
1018
1019    pub fn get_all_path_reference_overrides(
1020        &mut self,
1021        asset_id: AssetId,
1022    ) -> Option<&HashMap<CanonicalPathReference, AssetId>> {
1023        self.assets
1024            .get(&asset_id)
1025            .map(|x| &x.build_info.path_reference_overrides)
1026    }
1027
1028    pub fn set_path_reference_override(
1029        &mut self,
1030        asset_id: AssetId,
1031        path: CanonicalPathReference,
1032        referenced_asset_id: AssetId,
1033    ) -> DataSetResult<()> {
1034        let asset = self
1035            .assets
1036            .get_mut(&asset_id)
1037            .ok_or(DataSetError::AssetNotFound)?;
1038
1039        asset
1040            .build_info
1041            .path_reference_overrides
1042            .insert(path, referenced_asset_id);
1043        Ok(())
1044    }
1045
1046    pub fn asset_prototype(
1047        &self,
1048        asset_id: AssetId,
1049    ) -> Option<AssetId> {
1050        self.assets.get(&asset_id).map(|x| x.prototype).flatten()
1051    }
1052
1053    pub fn asset_schema(
1054        &self,
1055        asset_id: AssetId,
1056    ) -> Option<&SchemaRecord> {
1057        self.assets.get(&asset_id).map(|x| &x.schema)
1058    }
1059
1060    fn hash_property_data(
1061        hasher: &mut SipHasher,
1062        properties: &HashMap<String, Value>,
1063        property_null_overrides: &HashMap<String, NullOverride>,
1064        properties_in_replace_mode: &HashSet<String>,
1065        dynamic_collection_entries: &HashMap<String, OrderedSet<Uuid>>,
1066    ) {
1067        // properties
1068        let mut properties_hash = 0;
1069        for (key, value) in properties {
1070            let mut inner_hasher = siphasher::sip::SipHasher::default();
1071            key.hash(&mut inner_hasher);
1072            value.hash(&mut inner_hasher);
1073            properties_hash = properties_hash ^ inner_hasher.finish();
1074        }
1075        properties_hash.hash(hasher);
1076
1077        // property_null_overrides
1078        let mut property_null_overrides_hash = 0;
1079        for (key, value) in property_null_overrides {
1080            let mut inner_hasher = siphasher::sip::SipHasher::default();
1081            key.hash(&mut inner_hasher);
1082            value.hash(&mut inner_hasher);
1083            property_null_overrides_hash = property_null_overrides_hash ^ inner_hasher.finish();
1084        }
1085        property_null_overrides_hash.hash(hasher);
1086
1087        // properties_in_replace_mode
1088        let mut properties_in_replace_mode_hash = 0;
1089        for value in properties_in_replace_mode {
1090            let mut inner_hasher = siphasher::sip::SipHasher::default();
1091            value.hash(&mut inner_hasher);
1092            properties_in_replace_mode_hash =
1093                properties_in_replace_mode_hash ^ inner_hasher.finish();
1094        }
1095        properties_in_replace_mode_hash.hash(hasher);
1096
1097        // dynamic_collection_entries
1098        let mut dynamic_collection_entries_hash = 0;
1099        for (key, value) in dynamic_collection_entries {
1100            let mut inner_hasher = siphasher::sip::SipHasher::default();
1101            key.hash(&mut inner_hasher);
1102
1103            let mut uuid_set_hash = 0;
1104            for id in value.iter() {
1105                let mut inner_hasher2 = siphasher::sip::SipHasher::default();
1106                id.hash(&mut inner_hasher2);
1107                uuid_set_hash = uuid_set_hash ^ inner_hasher2.finish();
1108            }
1109            uuid_set_hash.hash(&mut inner_hasher);
1110
1111            dynamic_collection_entries_hash =
1112                dynamic_collection_entries_hash ^ inner_hasher.finish();
1113        }
1114        dynamic_collection_entries_hash.hash(hasher);
1115    }
1116
1117    pub fn hash_object(
1118        &self,
1119        asset_id: AssetId,
1120        hash_object_mode: HashObjectMode,
1121    ) -> DataSetResult<u64> {
1122        let asset = self
1123            .assets
1124            .get(&asset_id)
1125            .ok_or(DataSetError::AssetNotFound)?;
1126
1127        let mut hasher = SipHasher::default();
1128
1129        // This handles hashing the location chain for either ID-based or path-based storage
1130        match hash_object_mode {
1131            HashObjectMode::PropertiesOnly => {
1132                // Do nothing
1133            }
1134            HashObjectMode::FullObjectWithLocationId => {
1135                // ID-based storage would only care about the location ID changing
1136                asset.asset_location.path_node_id.hash(&mut hasher);
1137            }
1138            HashObjectMode::FullObjectWithLocationChainNames => {
1139                // Path-based storage cares about the names of the locations up the whole chain
1140                let location_chain = self.asset_location_chain(asset_id)?;
1141                for location in location_chain {
1142                    let location_asset = self
1143                        .assets
1144                        .get(&location.path_node_id)
1145                        .ok_or(DataSetError::AssetNotFound)?;
1146                    location_asset.asset_name.hash(&mut hasher);
1147                }
1148            }
1149        };
1150
1151        // Extra data for "full object" modes - essentially everything but location and properties
1152        match hash_object_mode {
1153            HashObjectMode::FullObjectWithLocationId
1154            | HashObjectMode::FullObjectWithLocationChainNames => {
1155                asset.asset_name.hash(&mut hasher);
1156                asset.import_info.hash(&mut hasher);
1157                asset.build_info.hash(&mut hasher);
1158                asset.prototype.hash(&mut hasher);
1159            }
1160            _ => {}
1161        }
1162
1163        // This data is hashed in all modes
1164        let schema = &asset.schema;
1165        schema.fingerprint().hash(&mut hasher);
1166
1167        // We always hash property data
1168        Self::hash_property_data(
1169            &mut hasher,
1170            &asset.properties,
1171            &asset.property_null_overrides,
1172            &asset.properties_in_replace_mode,
1173            &asset.dynamic_collection_entries,
1174        );
1175
1176        // Properties only mode hashes up the prototype chain
1177        if hash_object_mode == HashObjectMode::PropertiesOnly {
1178            if let Some(prototype) = asset.prototype {
1179                // We may fail to find the prototype, there is a good chance this means our data is in
1180                // a bad state, but it is not considered fatal. Generally in these circumstances we
1181                // carry on as if the prototype was set to None.
1182                self.hash_object(prototype, HashObjectMode::PropertiesOnly)?
1183                    .hash(&mut hasher);
1184            }
1185        }
1186
1187        let asset_hash = hasher.finish();
1188        Ok(asset_hash)
1189    }
1190
1191    /// Gets if the property has a null override associated with it *on this object* ignoring the
1192    /// prototype. An error will be returned if the asset doesn't exist, the schema doesn't exist,
1193    /// or if this field is not nullable
1194    pub fn get_null_override(
1195        &self,
1196        schema_set: &SchemaSet,
1197        asset_id: AssetId,
1198        path: impl AsRef<str>,
1199    ) -> DataSetResult<NullOverride> {
1200        let asset = self
1201            .assets
1202            .get(&asset_id)
1203            .ok_or(DataSetError::AssetNotFound)?;
1204        let property_schema = asset
1205            .schema
1206            .find_property_schema(&path, schema_set.schemas())
1207            .ok_or(DataSetError::SchemaNotFound)?;
1208
1209        if property_schema.is_nullable() {
1210            // Not existing in the map implies that it is unset
1211            Ok(asset
1212                .property_null_overrides
1213                .get(path.as_ref())
1214                .copied()
1215                .unwrap_or(NullOverride::Unset))
1216        } else {
1217            Err(DataSetError::InvalidSchema)?
1218        }
1219    }
1220
1221    /// Sets or removes the null override state on this object.
1222    pub fn set_null_override(
1223        &mut self,
1224        schema_set: &SchemaSet,
1225        asset_id: AssetId,
1226        path: impl AsRef<str>,
1227        null_override: NullOverride,
1228    ) -> DataSetResult<()> {
1229        let asset = self
1230            .assets
1231            .get_mut(&asset_id)
1232            .ok_or(DataSetError::AssetNotFound)?;
1233        let property_schema = asset
1234            .schema
1235            .find_property_schema(&path, schema_set.schemas())
1236            .ok_or(DataSetError::SchemaNotFound)?;
1237
1238        if property_schema.is_nullable() {
1239            if null_override != NullOverride::Unset {
1240                asset
1241                    .property_null_overrides
1242                    .insert(path.as_ref().to_string(), null_override);
1243            } else {
1244                // Not existing in the map implies that it is unset
1245                asset.property_null_overrides.remove(path.as_ref());
1246            }
1247            Ok(())
1248        } else {
1249            Err(DataSetError::InvalidSchema)?
1250        }
1251    }
1252
1253    fn validate_parent_paths(
1254        &self,
1255        schema_set: &SchemaSet,
1256        asset_id: AssetId,
1257        path: impl AsRef<str>,
1258    ) -> DataSetResult<Schema> {
1259        let asset_schema = self
1260            .asset_schema(asset_id)
1261            .ok_or(DataSetError::AssetNotFound)?;
1262
1263        // Contains the path segments that we need to check for being null
1264        let mut accessed_nullable_keys = vec![];
1265        // The containers we access and what keys are used to access them
1266        let mut accessed_dynamic_array_keys = vec![];
1267        let mut accessed_static_array_keys = vec![];
1268        let mut accessed_map_keys = vec![];
1269
1270        let schema = super::property_schema_and_path_ancestors_to_check(
1271            asset_schema,
1272            &path,
1273            schema_set.schemas(),
1274            &mut accessed_nullable_keys,
1275            &mut accessed_dynamic_array_keys,
1276            &mut accessed_static_array_keys,
1277            &mut accessed_map_keys,
1278        )?;
1279
1280        // See if this field was contained in any nullables. If any of those were null, return None.
1281        for checked_property in &accessed_nullable_keys {
1282            if self.resolve_null_override(schema_set, asset_id, checked_property)?
1283                != NullOverride::SetNonNull
1284            {
1285                return Err(DataSetError::PathParentIsNull)?;
1286            }
1287        }
1288
1289        // See if this field was contained in a container. If any of those containers didn't contain
1290        // this property path, return None
1291        for (path, key) in &accessed_dynamic_array_keys {
1292            let dynamic_collection_entries =
1293                self.resolve_dynamic_array_entries(schema_set, asset_id, path)?;
1294            if !dynamic_collection_entries
1295                .contains(&Uuid::from_str(key).map_err(|_| DataSetError::UuidParseError)?)
1296            {
1297                return Err(DataSetError::PathDynamicArrayEntryDoesNotExist)?;
1298            }
1299        }
1300
1301        Ok(schema)
1302    }
1303
1304    // None return means something higher in property hierarchy is null or non-existing
1305    pub fn resolve_null_override(
1306        &self,
1307        schema_set: &SchemaSet,
1308        asset_id: AssetId,
1309        path: impl AsRef<str>,
1310    ) -> DataSetResult<NullOverride> {
1311        let property_schema = self.validate_parent_paths(schema_set, asset_id, path.as_ref())?;
1312
1313        // This field is not nullable, return an error
1314        if !property_schema.is_nullable() {
1315            return Err(DataSetError::InvalidSchema)?;
1316        }
1317
1318        // Recursively look for a null override for this property being set. We can make a call
1319        let mut prototype_id = Some(asset_id);
1320        while let Some(prototype_id_iter) = prototype_id {
1321            let obj = self
1322                .assets
1323                .get(&prototype_id_iter)
1324                .ok_or(DataSetError::AssetNotFound)?;
1325
1326            if let Some(value) = obj.property_null_overrides.get(path.as_ref()) {
1327                match value {
1328                    // We do not put NullOverride::Unset in the property_null_overrides map
1329                    NullOverride::Unset => unreachable!(),
1330                    NullOverride::SetNull => return Ok(NullOverride::SetNull),
1331                    NullOverride::SetNonNull => return Ok(NullOverride::SetNonNull),
1332                }
1333            }
1334
1335            prototype_id = obj.prototype;
1336        }
1337
1338        // By default
1339        Ok(NullOverride::Unset)
1340    }
1341
1342    pub fn has_property_override(
1343        &self,
1344        asset_id: AssetId,
1345        path: impl AsRef<str>,
1346    ) -> DataSetResult<bool> {
1347        Ok(self.get_property_override(asset_id, path)?.is_some())
1348    }
1349
1350    // Just gets if this asset has a property without checking prototype chain for fallback or returning a default
1351    // Returning none means it is not overridden
1352    pub fn get_property_override(
1353        &self,
1354        asset_id: AssetId,
1355        path: impl AsRef<str>,
1356    ) -> DataSetResult<Option<&Value>> {
1357        let asset = self
1358            .assets
1359            .get(&asset_id)
1360            .ok_or(DataSetError::AssetNotFound)?;
1361        Ok(asset.properties.get(path.as_ref()))
1362    }
1363
1364    // Just sets a property on this asset, making it overridden, or replacing the existing override
1365    pub fn set_property_override(
1366        &mut self,
1367        schema_set: &SchemaSet,
1368        asset_id: AssetId,
1369        path: impl AsRef<str>,
1370        value: Option<Value>,
1371    ) -> DataSetResult<Option<Value>> {
1372        let asset_schema = self
1373            .asset_schema(asset_id)
1374            .ok_or(DataSetError::AssetNotFound)?;
1375        let property_schema = asset_schema
1376            .find_property_schema(&path, schema_set.schemas())
1377            .ok_or(DataSetError::SchemaNotFound)?;
1378
1379        if let Some(value) = &value {
1380            if !value.matches_schema(&property_schema, schema_set.schemas()) {
1381                log::debug!(
1382                    "Value {:?} doesn't match schema {:?} on schema {:?} path {:?}",
1383                    value,
1384                    property_schema,
1385                    asset_schema.name(),
1386                    path.as_ref()
1387                );
1388                return Err(DataSetError::ValueDoesNotMatchSchema)?;
1389            }
1390        }
1391
1392        let _ = self.validate_parent_paths(schema_set, asset_id, path.as_ref())?;
1393
1394        let obj = self
1395            .assets
1396            .get_mut(&asset_id)
1397            .ok_or(DataSetError::AssetNotFound)?;
1398        let old_value = if let Some(value) = value {
1399            obj.properties.insert(path.as_ref().to_string(), value)
1400        } else {
1401            obj.properties.remove(path.as_ref())
1402        };
1403        Ok(old_value)
1404    }
1405
1406    pub fn apply_property_override_to_prototype(
1407        &mut self,
1408        schema_set: &SchemaSet,
1409        asset_id: AssetId,
1410        path: impl AsRef<str>,
1411    ) -> DataSetResult<()> {
1412        let asset = self
1413            .assets
1414            .get(&asset_id)
1415            .ok_or(DataSetError::AssetNotFound)?;
1416        let prototype_id = asset.prototype;
1417
1418        if let Some(prototype_id) = prototype_id {
1419            let v = self.set_property_override(schema_set, asset_id, path.as_ref(), None)?;
1420            if let Some(v) = v {
1421                // The property existed on the child, set it on the prototype
1422                self.set_property_override(schema_set, prototype_id, path, Some(v))?;
1423            } else {
1424                // The property didn't exist on the child, do nothing
1425            }
1426        } else {
1427            // The asset has no prototype, do nothing
1428        }
1429
1430        Ok(())
1431    }
1432
1433    pub fn resolve_property<'a>(
1434        &'a self,
1435        schema_set: &'a SchemaSet,
1436        asset_id: AssetId,
1437        path: impl AsRef<str>,
1438    ) -> DataSetResult<&'a Value> {
1439        let property_schema = self.validate_parent_paths(schema_set, asset_id, path.as_ref())?;
1440
1441        let mut prototype_id = Some(asset_id);
1442        while let Some(prototype_id_iter) = prototype_id {
1443            let obj = self.assets.get(&prototype_id_iter);
1444            if let Some(obj) = obj {
1445                if let Some(value) = obj.properties.get(path.as_ref()) {
1446                    return Ok(value);
1447                }
1448
1449                prototype_id = obj.prototype;
1450            } else {
1451                // The prototype being referenced was not found, break out of the loop and pretend
1452                // like the prototype is unset
1453                prototype_id = None;
1454            }
1455        }
1456
1457        Ok(Value::default_for_schema(&property_schema, schema_set))
1458    }
1459
1460    fn get_dynamic_collection_entries(
1461        asset: &DataSetAssetInfo,
1462        path: impl AsRef<str>,
1463    ) -> DataSetResult<std::slice::Iter<Uuid>> {
1464        if let Some(overrides) = asset.dynamic_collection_entries.get(path.as_ref()) {
1465            Ok(overrides.iter())
1466        } else {
1467            Ok(std::slice::Iter::default())
1468        }
1469    }
1470
1471    pub fn get_dynamic_array_entries(
1472        &self,
1473        schema_set: &SchemaSet,
1474        asset_id: AssetId,
1475        path: impl AsRef<str>,
1476    ) -> DataSetResult<std::slice::Iter<Uuid>> {
1477        let asset = self
1478            .assets
1479            .get(&asset_id)
1480            .ok_or(DataSetError::AssetNotFound)?;
1481        let property_schema = asset
1482            .schema
1483            .find_property_schema(&path, schema_set.schemas())
1484            .ok_or(DataSetError::SchemaNotFound)?;
1485
1486        if !property_schema.is_dynamic_array() {
1487            return Err(DataSetError::InvalidSchema)?;
1488        }
1489
1490        Self::get_dynamic_collection_entries(asset, path)
1491    }
1492
1493    pub fn get_map_entries(
1494        &self,
1495        schema_set: &SchemaSet,
1496        asset_id: AssetId,
1497        path: impl AsRef<str>,
1498    ) -> DataSetResult<std::slice::Iter<Uuid>> {
1499        let asset = self
1500            .assets
1501            .get(&asset_id)
1502            .ok_or(DataSetError::AssetNotFound)?;
1503        let property_schema = asset
1504            .schema
1505            .find_property_schema(&path, schema_set.schemas())
1506            .ok_or(DataSetError::SchemaNotFound)?;
1507
1508        if !property_schema.is_map() {
1509            return Err(DataSetError::InvalidSchema)?;
1510        }
1511
1512        Self::get_dynamic_collection_entries(asset, path)
1513    }
1514
1515    fn add_dynamic_collection_entry(
1516        asset: &mut DataSetAssetInfo,
1517        path: impl AsRef<str>,
1518    ) -> DataSetResult<Uuid> {
1519        let entry = asset
1520            .dynamic_collection_entries
1521            .entry(path.as_ref().to_string())
1522            .or_insert(Default::default());
1523        let new_uuid = Uuid::new_v4();
1524        let newly_inserted = entry.try_insert_at_end(new_uuid);
1525        if !newly_inserted {
1526            panic!("Created a new random UUID but it matched an existing UUID");
1527        }
1528        Ok(new_uuid)
1529    }
1530
1531    pub fn add_dynamic_array_entry(
1532        &mut self,
1533        schema_set: &SchemaSet,
1534        asset_id: AssetId,
1535        path: impl AsRef<str>,
1536    ) -> DataSetResult<Uuid> {
1537        let asset = self
1538            .assets
1539            .get_mut(&asset_id)
1540            .ok_or(DataSetError::AssetNotFound)?;
1541        let property_schema = asset
1542            .schema
1543            .find_property_schema(&path, schema_set.schemas())
1544            .ok_or(DataSetError::SchemaNotFound)?;
1545
1546        if !property_schema.is_dynamic_array() {
1547            return Err(DataSetError::InvalidSchema)?;
1548        }
1549
1550        Self::add_dynamic_collection_entry(asset, path)
1551    }
1552
1553    pub fn add_map_entry(
1554        &mut self,
1555        schema_set: &SchemaSet,
1556        asset_id: AssetId,
1557        path: impl AsRef<str>,
1558    ) -> DataSetResult<Uuid> {
1559        let asset = self
1560            .assets
1561            .get_mut(&asset_id)
1562            .ok_or(DataSetError::AssetNotFound)?;
1563        let property_schema = asset
1564            .schema
1565            .find_property_schema(&path, schema_set.schemas())
1566            .ok_or(DataSetError::SchemaNotFound)?;
1567
1568        if !property_schema.is_map() {
1569            return Err(DataSetError::InvalidSchema)?;
1570        }
1571
1572        Self::add_dynamic_collection_entry(asset, path)
1573    }
1574
1575    pub fn insert_dynamic_array_entry(
1576        &mut self,
1577        schema_set: &SchemaSet,
1578        asset_id: AssetId,
1579        path: impl AsRef<str>,
1580        index: usize,
1581        entry_uuid: Uuid,
1582    ) -> DataSetResult<()> {
1583        let asset = self
1584            .assets
1585            .get_mut(&asset_id)
1586            .ok_or(DataSetError::AssetNotFound)?;
1587        let property_schema = asset
1588            .schema
1589            .find_property_schema(&path, schema_set.schemas())
1590            .ok_or(DataSetError::SchemaNotFound)?;
1591
1592        if !property_schema.is_dynamic_array() {
1593            return Err(DataSetError::InvalidSchema)?;
1594        }
1595
1596        let entry = asset
1597            .dynamic_collection_entries
1598            .entry(path.as_ref().to_string())
1599            .or_insert(Default::default());
1600        if entry.try_insert_at_position(index, entry_uuid) {
1601            Ok(())
1602        } else {
1603            Err(DataSetError::DuplicateEntryKey)?
1604        }
1605    }
1606
1607    fn remove_dynamic_collection_entry(
1608        asset: &mut DataSetAssetInfo,
1609        path: impl AsRef<str>,
1610        element_id: Uuid,
1611    ) -> DataSetResult<bool> {
1612        if let Some(override_list) = asset.dynamic_collection_entries.get_mut(path.as_ref()) {
1613            // Return if the override existed or not
1614            let was_removed = override_list.remove(&element_id);
1615            Ok(was_removed)
1616        } else {
1617            // The override didn't exist
1618            Ok(false)
1619        }
1620    }
1621
1622    pub fn remove_dynamic_array_entry(
1623        &mut self,
1624        schema_set: &SchemaSet,
1625        asset_id: AssetId,
1626        path: impl AsRef<str>,
1627        element_id: Uuid,
1628    ) -> DataSetResult<bool> {
1629        let asset = self
1630            .assets
1631            .get_mut(&asset_id)
1632            .ok_or(DataSetError::AssetNotFound)?;
1633        let property_schema = asset
1634            .schema
1635            .find_property_schema(&path, schema_set.schemas())
1636            .ok_or(DataSetError::SchemaNotFound)?;
1637
1638        if !property_schema.is_dynamic_array() {
1639            return Err(DataSetError::InvalidSchema)?;
1640        }
1641
1642        Self::remove_dynamic_collection_entry(asset, path, element_id)
1643    }
1644
1645    pub fn remove_map_entry(
1646        &mut self,
1647        schema_set: &SchemaSet,
1648        asset_id: AssetId,
1649        path: impl AsRef<str>,
1650        element_id: Uuid,
1651    ) -> DataSetResult<bool> {
1652        let asset = self
1653            .assets
1654            .get_mut(&asset_id)
1655            .ok_or(DataSetError::AssetNotFound)?;
1656        let property_schema = asset
1657            .schema
1658            .find_property_schema(&path, schema_set.schemas())
1659            .ok_or(DataSetError::SchemaNotFound)?;
1660
1661        if !property_schema.is_map() {
1662            return Err(DataSetError::InvalidSchema)?;
1663        }
1664
1665        Self::remove_dynamic_collection_entry(asset, path, element_id)
1666    }
1667
1668    fn do_resolve_dynamic_collection_entries(
1669        &self,
1670        asset_id: AssetId,
1671        path: &str,
1672        resolved_entries: &mut Vec<Uuid>,
1673    ) -> DataSetResult<()> {
1674        let obj = self
1675            .assets
1676            .get(&asset_id)
1677            .ok_or(DataSetError::AssetNotFound)?;
1678
1679        // See if any properties in the path ancestry are replacing parent data
1680        let mut check_parents = true;
1681
1682        if obj.properties_in_replace_mode.contains(path) {
1683            check_parents = false;
1684        }
1685
1686        // If we do not replace parent data, resolve it now so we can append to it
1687        if check_parents {
1688            if let Some(prototype) = obj.prototype {
1689                // If the prototype is not found, we behave as if the prototype was not set
1690                if self.assets.contains_key(&prototype) {
1691                    self.do_resolve_dynamic_collection_entries(prototype, path, resolved_entries)?;
1692                }
1693            }
1694        }
1695
1696        if let Some(entries) = obj.dynamic_collection_entries.get(path) {
1697            for entry in entries {
1698                resolved_entries.push(*entry);
1699            }
1700        }
1701
1702        Ok(())
1703    }
1704
1705    pub fn resolve_dynamic_array_entries(
1706        &self,
1707        schema_set: &SchemaSet,
1708        asset_id: AssetId,
1709        path: impl AsRef<str>,
1710    ) -> DataSetResult<Box<[Uuid]>> {
1711        let property_schema = self.validate_parent_paths(schema_set, asset_id, path.as_ref())?;
1712        if !property_schema.is_dynamic_array() {
1713            return Err(DataSetError::InvalidSchema)?;
1714        }
1715
1716        let mut resolved_entries = vec![];
1717        self.do_resolve_dynamic_collection_entries(asset_id, path.as_ref(), &mut resolved_entries)?;
1718        Ok(resolved_entries.into_boxed_slice())
1719    }
1720
1721    pub fn resolve_map_entries(
1722        &self,
1723        schema_set: &SchemaSet,
1724        asset_id: AssetId,
1725        path: impl AsRef<str>,
1726    ) -> DataSetResult<Box<[Uuid]>> {
1727        let property_schema = self.validate_parent_paths(schema_set, asset_id, path.as_ref())?;
1728        if !property_schema.is_map() {
1729            return Err(DataSetError::InvalidSchema)?;
1730        }
1731
1732        let mut resolved_entries = vec![];
1733        self.do_resolve_dynamic_collection_entries(asset_id, path.as_ref(), &mut resolved_entries)?;
1734        Ok(resolved_entries.into_boxed_slice())
1735    }
1736
1737    pub fn get_override_behavior(
1738        &self,
1739        schema_set: &SchemaSet,
1740        asset_id: AssetId,
1741        path: impl AsRef<str>,
1742    ) -> DataSetResult<OverrideBehavior> {
1743        let asset = self
1744            .assets
1745            .get(&asset_id)
1746            .ok_or(DataSetError::AssetNotFound)?;
1747        let property_schema = asset
1748            .schema
1749            .find_property_schema(&path, schema_set.schemas())
1750            .ok_or(DataSetError::SchemaNotFound)?;
1751
1752        Ok(match property_schema {
1753            Schema::DynamicArray(_) | Schema::Map(_) => {
1754                if asset.properties_in_replace_mode.contains(path.as_ref()) {
1755                    OverrideBehavior::Replace
1756                } else {
1757                    OverrideBehavior::Append
1758                }
1759            }
1760            _ => OverrideBehavior::Replace,
1761        })
1762    }
1763
1764    pub fn set_override_behavior(
1765        &mut self,
1766        schema_set: &SchemaSet,
1767        asset_id: AssetId,
1768        path: impl AsRef<str>,
1769        behavior: OverrideBehavior,
1770    ) -> DataSetResult<()> {
1771        let asset = self
1772            .assets
1773            .get_mut(&asset_id)
1774            .ok_or(DataSetError::AssetNotFound)?;
1775        let property_schema = asset
1776            .schema
1777            .find_property_schema(&path, schema_set.schemas())
1778            .ok_or(DataSetError::SchemaNotFound)?;
1779
1780        match property_schema {
1781            Schema::DynamicArray(_) | Schema::Map(_) => {
1782                let _ = match behavior {
1783                    OverrideBehavior::Append => {
1784                        asset.properties_in_replace_mode.remove(path.as_ref())
1785                    }
1786                    OverrideBehavior::Replace => asset
1787                        .properties_in_replace_mode
1788                        .insert(path.as_ref().to_string()),
1789                };
1790                Ok(())
1791            }
1792            _ => Err(DataSetError::InvalidSchema)?,
1793        }
1794    }
1795
1796    pub fn read_properties_bundle(
1797        &self,
1798        schema_set: &SchemaSet,
1799        asset_id: AssetId,
1800        path: impl AsRef<str>,
1801    ) -> DataSetResult<PropertiesBundle> {
1802        let asset = self
1803            .assets
1804            .get(&asset_id)
1805            .ok_or(DataSetError::AssetNotFound)?;
1806        Ok(PropertiesBundle::read(asset, path, schema_set)?)
1807    }
1808
1809    pub fn write_properties_bundle(
1810        &mut self,
1811        schema_set: &SchemaSet,
1812        asset_id: AssetId,
1813        path: impl AsRef<str>,
1814        properties_bundle: &PropertiesBundle,
1815    ) -> DataSetResult<()> {
1816        let asset = self
1817            .assets
1818            .get_mut(&asset_id)
1819            .ok_or(DataSetError::AssetNotFound)?;
1820        properties_bundle.write(asset, path, schema_set)
1821    }
1822}