Skip to main content

rocraters/ro_crate/
modify.rs

1//! Module for interacting with dynamic entities
2//!
3//! A dynamic entity is simply a field that is not a MUST within a defined
4//! RO-Crate entity. It can be added, modified or deleted.
5
6use crate::ro_crate::constraints::*;
7use log::debug;
8use serde::ser::SerializeMap;
9use serde::{Serialize, Serializer};
10use serde_json::Value;
11use std::collections::HashMap;
12
13/// A trait for manipulating dynamic entities within an RO-Crate object.
14///
15/// Provides methods for adding, removing, and searching dynamic fields and values
16/// within entities of an RO-Crate. These dynamic entities are often represented as
17/// flexible structures, allowing for a varied and extensible set of metadata.
18///
19/// # Note
20/// Additions need to be updated due to evolution of the EntityValue type. There
21/// will be some redundency.
22pub trait DynamicEntityManipulation: Serialize {
23    /// Gets a mutable reference to the dynamic entity's underlying HashMap.
24    fn dynamic_entity(&mut self) -> &mut Option<HashMap<String, EntityValue>>;
25
26    /// Gets an immutable reference to the dynamic entity's underlying HashMap.
27    fn dynamic_entity_immut(&self) -> &Option<HashMap<String, EntityValue>>;
28
29    /// Adds dynamic fields to the entity
30    ///
31    /// # Arguments
32    /// * 'values' - A 'Hashmap' containing a key and a valid EntityValue type
33    fn add_dynamic_fields(&mut self, values: HashMap<String, EntityValue>) {
34        for (key, value) in values {
35            match value {
36                EntityValue::EntityString(s) => self.add_string_value(key, s),
37                EntityValue::EntityId(id) => self.add_id_value(key, id),
38                _ => todo!(),
39            }
40        }
41    }
42
43    /// Adds a string value to the dynamic entity.
44    ///
45    /// # Arguments
46    /// * `key` - The field name as a `String`.
47    /// * `value` - The string value to be added.
48    fn add_string_value(&mut self, key: String, value: String) {
49        // Use `entry` API for a more concise approach
50        // This will automatically create the HashMap if it doesn't exist
51        self.dynamic_entity()
52            .get_or_insert_with(HashMap::new)
53            .insert(key, EntityValue::EntityString(value));
54    }
55
56    /// Adds an identifier value to the dynamic entity.
57    ///
58    /// # Arguments
59    /// * `key` - The field name as a `String`.
60    /// * `value` - The `Id` value to be added.    
61    fn add_id_value(&mut self, key: String, value: Id) {
62        self.dynamic_entity()
63            .get_or_insert_with(HashMap::new)
64            .insert(key, EntityValue::EntityId(value));
65    }
66
67    /// Removes a field from the dynamic entity.
68    ///
69    /// # Arguments
70    /// * `key` - The name of the field to remove.
71    fn remove_field(&mut self, key: &str) {
72        if let Some(dynamic_entity) = self.dynamic_entity() {
73            dynamic_entity.remove(key);
74        }
75    }
76
77    /// Searches for a specific value within the dynamic entity.
78    ///
79    /// # Arguments
80    /// * `search_value` - The `EntityValue` value to search for.
81    ///
82    /// # Returns
83    /// `true` if the value is found, otherwise `false`.
84    fn search_value(&self, search_value: &EntityValue) -> bool {
85        if let Some(dynamic_entity) = self.dynamic_entity_immut() {
86            for (_key, value) in dynamic_entity.iter() {
87                if value == search_value {
88                    return true;
89                }
90            }
91        }
92        false
93    }
94
95    /// Finds keys within the RO-Crate matching a specified key or retrieves all keys.
96    ///
97    /// # Arguments
98    /// * `search_key` - A `String` specifying the key to search for.
99    /// * `get_all` - A boolean indicating whether to return all keys or only those matching `search_key`.
100    ///
101    /// # Returns
102    /// A vector of strings containing the keys found.
103    fn search_properties_for_value(&self, search_property: &str) -> Option<EntityValue> {
104        /// Recursive function for traversing nested objects.
105        fn search_obj(
106            object: &HashMap<String, EntityValue>,
107            search_property: &str,
108        ) -> Option<EntityValue> {
109            for (key, value) in object.iter() {
110                if key == search_property {
111                    return Some(value.clone());
112                }
113                if let EntityValue::EntityObject(inner_object) = value {
114                    if let Some(result) = search_obj(inner_object, search_property) {
115                        return Some(result);
116                    }
117                }
118            }
119            None
120        }
121
122        // Search in the dynamic_entity field.
123        if let Some(dynamic_entity) = self.dynamic_entity_immut() {
124            for (key, value) in dynamic_entity.iter() {
125                if key == search_property {
126                    return Some(value.clone());
127                }
128                if let EntityValue::EntityObject(object) = value {
129                    if let Some(result) = search_obj(object, search_property) {
130                        return Some(result);
131                    }
132                }
133            }
134        }
135        None
136    }
137
138    /// Get all keys containing within a dynamic_entity
139    fn get_all_keys(&self) -> Vec<String> {
140        let mut key_vec: Vec<String> = Vec::new();
141
142        /// recursive function for traversing nested objects.
143        /// Should probably move out from nest
144        fn search_nested_object(object: &HashMap<String, EntityValue>) -> Vec<String> {
145            let mut key_vec: Vec<String> = Vec::new();
146            for (_key, value) in object.iter() {
147                key_vec.push(_key.clone());
148
149                if let EntityValue::EntityObject(inner_object) = value {
150                    let inner_keys = search_nested_object(inner_object);
151                    if !inner_keys.is_empty() {
152                        key_vec.extend(inner_keys);
153                    }
154                }
155            }
156            key_vec
157        }
158
159        if let Some(dynamic_entity) = self.dynamic_entity_immut() {
160            for (_key, value) in dynamic_entity.iter() {
161                if let EntityValue::EntityObject(object) = value {
162                    let obvec = search_nested_object(object);
163                    if !obvec.is_empty() {
164                        key_vec.extend(obvec);
165                    }
166                }
167
168                key_vec.push(_key.to_string());
169            }
170        }
171        key_vec
172    }
173
174    /// Method to remove a matching value from the dynamic_entity field.
175    ///
176    /// This can be both a useful and risky tool to use to clean a crate of a particular ID reference
177    /// Only ID related entity types and fallback values are allowed to be removed, as these encapsulate
178    /// usage of ID which is expected to be 99% of recursive removal. Other types which could be regularly
179    /// duplicated and non specific are ignored.
180    ///
181    /// # Arguements
182    /// * 'target_id' - A string representing the id value to be removed
183    ///
184    /// This is used as part of RoCrate implmentation for removing by ID
185    ///
186    /// Types that are not allowed to be recursively removed:
187    /// Bool  
188    /// f64
189    /// i64
190    ///
191    /// It does not allow you to remove values from fields that are defined as MUST within the Ro-Crate spec.
192    fn remove_matching_value(&mut self, target_id: &str) {
193        if let Some(dynamic_entity) = self.dynamic_entity() {
194            let mut keys_to_remove = Vec::new();
195
196            // Collect keys where Fallback values need modification
197            let mut fallback_keys_to_modify = Vec::new();
198            let mut updates = Vec::new();
199
200            for (key, value) in dynamic_entity.iter() {
201                match value {
202                    EntityValue::EntityString(s) if s == target_id => {
203                        keys_to_remove.push(key.clone());
204                    }
205                    EntityValue::EntityId(Id::IdArray(id_values)) => {
206                        let filtered_values: Vec<String> = id_values
207                            .iter()
208                            .filter(|id_val| id_val != &target_id)
209                            .cloned()
210                            .collect();
211
212                        if filtered_values.len() != id_values.len() {
213                            updates.push((key.clone(), Id::IdArray(filtered_values)));
214                        }
215                    }
216                    EntityValue::EntityId(Id::Id(id_value)) if id_value == target_id => {
217                        keys_to_remove.push(key.clone());
218                    }
219                    EntityValue::Fallback(fallback_values) => {
220                        debug!("Exploring fallback {:?}", fallback_values);
221                        fallback_keys_to_modify.push(key.clone());
222                    }
223                    // Handle other EntityValue types if necessary
224                    _ => {
225                        //println!("Unknown type {:?}?", value);
226                    }
227                }
228            }
229
230            // Potentially depreciated need to test
231            for key in fallback_keys_to_modify {
232                if let Some(EntityValue::Fallback(fallback_value)) = dynamic_entity.get_mut(&key) {
233                    if let Some(fallback_value) = fallback_value.as_mut() {
234                        remove_matching_value_from_json(fallback_value, target_id);
235                    }
236                }
237            }
238
239            // For entity ID's in a vec
240            for (key, updated_id) in updates {
241                if let Some(EntityValue::EntityId(id)) = dynamic_entity.get_mut(&key) {
242                    *id = updated_id;
243                }
244            }
245
246            // Remove the keys that have direct string matches
247            for key in keys_to_remove {
248                dynamic_entity.remove(&key);
249            }
250        }
251    }
252    /// Updates all occurrences of a specific ID with a new ID within the dynamic entity fields.
253    ///
254    /// This method traverses the dynamic entity fields of an object implementing `DynamicEntityManipulation`,
255    /// looking for any entity IDs that match `id_old` and replaces them with `id_new`. It supports updating
256    /// both single IDs and IDs within arrays.
257    ///
258    /// # Arguments
259    /// * `id_old` - A string slice representing the old ID to be replaced.
260    /// * `id_new` - A string slice representing the new ID to replace with.
261    ///
262    /// # Returns
263    /// Returns `Some(())` if at least one ID was updated, indicating that the operation made changes to the entity.
264    /// Returns `None` if no matching IDs were found or no updates were made.
265    ///
266    /// # Examples
267    /// Assuming `entity` is a member of the GraphVector Enum (e.g. GraphVector::DataEntity)
268    /// and contains an ID that matches `id_old`:
269    /// ```
270    /// if entity.update_matching_id("old_id", "new_id").is_some() {
271    ///     println!("ID updated successfully.");
272    /// } else {
273    ///     println!("No matching ID found or no update needed.");
274    /// }
275    /// ```
276    /// # Notes
277    /// This function is essential for maintaining referential integrity within the RO-Crate when IDs of entities
278    /// are changed. It ensures that all references to the updated entity reflect the new ID.
279    fn update_matching_id(&mut self, id_old: &str, id_new: &str) -> Option<bool> {
280        let mut updated = false;
281
282        if let Some(dynamic_entity) = &mut self.dynamic_entity() {
283            for (_key, value) in dynamic_entity.iter_mut() {
284                match value {
285                    EntityValue::EntityId(Id::IdArray(ids)) => {
286                        for id in ids.iter_mut() {
287                            if id == id_old {
288                                *id = id_new.to_string();
289                                updated = true;
290                            }
291                        }
292                    }
293                    EntityValue::EntityId(Id::Id(id)) => {
294                        if id == id_old {
295                            *id = id_new.to_string();
296                            updated = true;
297                        }
298                    }
299                    _ => (),
300                }
301            }
302        }
303
304        if updated { Some(true) } else { None }
305    }
306}
307
308/// Recursively removes matching values from fallback serailised json objects
309///
310/// This allows for nested id's to be removed indefinitely from complicated json objects that don't conform
311/// to a statically defined EntityValue Type. Both single objects and array objects are searched.
312///
313/// # Arguments
314/// * `value` - A mutable reference to a serde_json `Value` that represents the JSON structure to be cleaned.
315/// * `target_value` - The value to be searched for and removed from the JSON structure.
316///
317/// # Notes
318/// Potentially depreciated after EntityValue Type expansion
319/// TODO: Need to test
320fn remove_matching_value_from_json(value: &mut Value, target_value: &str) {
321    match value {
322        Value::Object(obj) => {
323            let keys_to_remove: Vec<String> = obj
324                .iter()
325                .filter_map(|(k, v)| {
326                    if v == target_value {
327                        Some(k.clone())
328                    } else {
329                        None
330                    }
331                })
332                .collect();
333
334            for key in keys_to_remove {
335                obj.remove(&key);
336            }
337
338            for v in obj.values_mut() {
339                remove_matching_value_from_json(v, target_value);
340            }
341        }
342        Value::Array(arr) => {
343            let mut i = 0;
344            while i < arr.len() {
345                if arr[i] == target_value {
346                    arr.remove(i);
347                } else {
348                    remove_matching_value_from_json(&mut arr[i], target_value);
349                    i += 1;
350                }
351            }
352        }
353        _ => {}
354    }
355}
356
357/// Returns key from dynamic_entity
358pub fn search_dynamic_entity_for_key(
359    dynamic_entity: &HashMap<String, EntityValue>,
360    target_value: &EntityValue,
361) -> Option<String> {
362    for (key, value) in dynamic_entity.iter() {
363        if value == target_value {
364            return Some(key.clone());
365        }
366
367        // If the value is a nested object, search recursively
368        if let EntityValue::EntityObject(inner_object) = value {
369            if let Some(inner_key) = search_dynamic_entity_for_key(inner_object, target_value) {
370                return Some(inner_key);
371            }
372        }
373    }
374
375    None
376}
377/// A trait for custom serialization of complex data structures.
378///
379/// `CustomSerialize` extends the standard `Serialize` trait to provide additional
380/// functionality for serializing data structures that contain both static and dynamic /// fields.
381///
382/// # Note
383/// This is utilised by both DataEntity and ContextualEntity
384pub trait CustomSerialize: Serialize {
385    fn dynamic_entity(&self) -> Option<&HashMap<String, EntityValue>>;
386    fn id(&self) -> &String;
387    fn type_(&self) -> &DataType;
388
389    fn custom_serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
390    where
391        S: Serializer,
392    {
393        let mut map = serializer.serialize_map(None)?;
394
395        map.serialize_entry("@id", self.id())?;
396        map.serialize_entry("@type", self.type_())?;
397
398        if let Some(dynamic_entity) = self.dynamic_entity() {
399            for (k, v) in dynamic_entity {
400                map.serialize_entry(k, v)?;
401            }
402        }
403
404        map.end()
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411    use serde_json::json;
412
413    struct TestEntity {
414        pub id: String,
415        pub type_: DataType,
416        pub dynamic_entity: Option<HashMap<String, EntityValue>>,
417    }
418
419    impl DynamicEntityManipulation for TestEntity {
420        fn dynamic_entity(&mut self) -> &mut Option<HashMap<String, EntityValue>> {
421            &mut self.dynamic_entity
422        }
423        fn dynamic_entity_immut(&self) -> &Option<HashMap<String, EntityValue>> {
424            &self.dynamic_entity
425        }
426    }
427
428    impl CustomSerialize for TestEntity {
429        fn dynamic_entity(&self) -> Option<&HashMap<String, EntityValue>> {
430            self.dynamic_entity.as_ref()
431        }
432
433        fn id(&self) -> &String {
434            &self.id
435        }
436
437        fn type_(&self) -> &DataType {
438            &self.type_
439        }
440    }
441
442    impl Serialize for TestEntity {
443        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
444        where
445            S: Serializer,
446        {
447            self.custom_serialize(serializer)
448        }
449    }
450
451    #[test]
452    fn test_add_string_value() {
453        let mut entity = TestEntity {
454            id: "test_id".to_string(),
455            type_: DataType::Term("test_type".to_string()),
456            dynamic_entity: None,
457        };
458
459        entity.add_string_value("key1".to_string(), "value1".to_string());
460        assert_eq!(
461            entity.dynamic_entity.unwrap().get("key1").unwrap(),
462            &EntityValue::EntityString("value1".to_string())
463        );
464    }
465
466    #[test]
467    fn test_remove_field() {
468        let mut entity = TestEntity {
469            id: "test_id".to_string(),
470            type_: DataType::Term("test_type".to_string()),
471            dynamic_entity: Some(HashMap::from([(
472                "key1".to_string(),
473                EntityValue::EntityString("value1".to_string()),
474            )])),
475        };
476
477        entity.remove_field("key1");
478        assert!(entity.dynamic_entity.unwrap().get("key1").is_none());
479    }
480
481    #[test]
482    fn test_remove_matching_value() {
483        let mut entity = TestEntity {
484            id: "test_id".to_string(),
485            type_: DataType::Term("test_type".to_string()),
486            dynamic_entity: Some(HashMap::from([
487                (
488                    "key1".to_string(),
489                    EntityValue::EntityString("value1".to_string()),
490                ),
491                (
492                    "key2".to_string(),
493                    EntityValue::EntityString("value1".to_string()),
494                ),
495            ])),
496        };
497
498        entity.remove_matching_value("value1");
499        assert!(entity.dynamic_entity.unwrap().is_empty());
500    }
501
502    #[test]
503    fn test_custom_serialize() {
504        let entity = TestEntity {
505            id: "test_id".to_string(),
506            type_: DataType::Term("test_type".to_string()),
507            dynamic_entity: Some(HashMap::from([(
508                "key1".to_string(),
509                EntityValue::EntityString("value1".to_string()),
510            )])),
511        };
512
513        let serialized = serde_json::to_string(&entity).unwrap();
514        let expected_json = json!({
515            "@id": "test_id",
516            "@type": "test_type",
517            "key1": "value1",
518        })
519        .to_string();
520
521        assert_eq!(serialized, expected_json);
522    }
523}