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}