Skip to main content

rocraters/ro_crate/
context.rs

1use log::debug;
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4use std::collections::HashMap;
5use uuid::Uuid;
6
7use crate::ro_crate::schema::{self, RoCrateSchemaVersion};
8/// Defines the JSON-LD contexts in an RO-Crate, facilitating flexible context specification.
9///
10/// This enum models the `@context` field's variability in RO-Crates, enabling the use of external URLs,
11/// combination of contexts, or embedded definitions directly within the crate. It supports:
12#[derive(Serialize, Deserialize, Debug, Clone)]
13#[serde(untagged)]
14pub enum RoCrateContext {
15    /// A URI string for referencing external JSON-LD contexts (default should be
16    /// ro-crate context).
17    ReferenceContext(String),
18    /// A combination of contexts for extended or customized vocabularies, represented as a list of items.
19    ExtendedContext(Vec<ContextItem>),
20    /// Directly embedded context definitions, ensuring crate portability by using a vector of hash maps for term definitions.    
21    EmbeddedContext(Vec<HashMap<String, String>>),
22}
23
24/// Represents elements in the `@context` of an RO-Crate, allowing for different ways to define terms.
25///
26/// There are two types of items:
27///
28/// - `ReferenceItem`: A URL string that links to an external context definition. It's like a reference to a standard set of terms used across different crates.
29///
30/// - `EmbeddedContext`: A map containing definitions directly. This is for defining terms right within the crate, making it self-contained.
31///
32#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
33#[serde(untagged)]
34pub enum ContextItem {
35    /// A URI string for referencing external JSON-LD contexts
36    ReferenceItem(String),
37    /// Directly embedded context definitions, ensureing crate protability by using a vector of
38    /// hash maps for term definitions
39    EmbeddedContext(HashMap<String, String>),
40}
41
42#[derive(Error, Debug)]
43pub enum ContextError {
44    #[error("{0}")]
45    NotFound(String),
46}
47
48impl RoCrateContext {
49    /// Adds a new context to the `RoCrateContext`.
50    pub fn add_context(&mut self, new_context: &ContextItem) {
51        match self {
52            RoCrateContext::ReferenceContext(current) => {
53                // Convert to `ExtendedContext` if necessary
54                *self = RoCrateContext::ExtendedContext(vec![
55                    ContextItem::ReferenceItem(current.clone()),
56                    new_context.clone(),
57                ]);
58            }
59            RoCrateContext::ExtendedContext(contexts) => {
60                // Add the new item to the extended context
61                contexts.push(new_context.clone());
62            }
63            RoCrateContext::EmbeddedContext(embedded_contexts) => {
64                // Merge new key-value pairs into the existing embedded context
65                if let ContextItem::EmbeddedContext(new_map) = new_context {
66                    embedded_contexts.push(new_map.clone());
67                }
68            }
69        }
70    }
71
72    /// Removes a context item from the `RoCrateContext`.
73    pub fn remove_context(&mut self, target: &ContextItem) -> Result<(), String> {
74        match self {
75            RoCrateContext::ReferenceContext(_) => Err(
76                "ReferenceContext cannot be removed as the crate requires a context.".to_string(),
77            ),
78            RoCrateContext::ExtendedContext(contexts) => {
79                let initial_len = contexts.len();
80                contexts.retain(|item| item != target);
81                if contexts.len() < initial_len {
82                    Ok(())
83                } else {
84                    Err("Context item not found in ExtendedContext.".to_string())
85                }
86            }
87            RoCrateContext::EmbeddedContext(embedded_contexts) => {
88                if let ContextItem::EmbeddedContext(target_map) = target {
89                    let initial_len = embedded_contexts.len();
90                    embedded_contexts.retain(|map| map != target_map);
91                    if embedded_contexts.len() < initial_len {
92                        Ok(())
93                    } else {
94                        Err("Target map not found in EmbeddedContext.".to_string())
95                    }
96                } else {
97                    Err("Invalid target type for EmbeddedContext.".to_string())
98                }
99            }
100        }
101    }
102
103    pub fn replace_context(
104        &mut self,
105        new_context: &ContextItem,
106        old_context: &ContextItem,
107    ) -> Result<(), ContextError> {
108        match self.remove_context(old_context) {
109            Ok(_removed) => {
110                self.add_context(new_context);
111                Ok(())
112            }
113            Err(e) => Err(ContextError::NotFound(e.to_string())),
114        }
115    }
116
117    pub fn get_all_context(&self) -> Vec<String> {
118        let mut valid_context: Vec<String> = Vec::new();
119        debug!("Self: {:?}", self);
120        match &self {
121            RoCrateContext::EmbeddedContext(context) => {
122                debug!("Found Embedded Context");
123                for map in context {
124                    for key in map.keys() {
125                        valid_context.push(key.to_string());
126                    }
127                }
128            }
129            RoCrateContext::ExtendedContext(context) => {
130                debug!("Found Extended Context");
131                for map in context {
132                    debug!("This is current map: {:?}", map);
133                    match map {
134                        ContextItem::EmbeddedContext(context) => {
135                            debug!("Inside Embedded Context: {:?}", context);
136                            for value in context.values() {
137                                valid_context.push(value.to_string());
138                            }
139                        }
140                        ContextItem::ReferenceItem(context) => {
141                            debug!("Inside Reference Item: {:?}", context);
142                            valid_context.push(context.to_string());
143                        }
144                    }
145                }
146            }
147            RoCrateContext::ReferenceContext(context) => {
148                debug!("Found Reference Context");
149                valid_context.push(context.to_string());
150            }
151        }
152        valid_context
153    }
154
155    pub fn get_specific_context(&self, context_key: &str) -> Option<String> {
156        match self {
157            RoCrateContext::ExtendedContext(context) => {
158                for item in context {
159                    match item {
160                        ContextItem::EmbeddedContext(embedded) => {
161                            if let Some(value) = embedded.get(context_key) {
162                                return Some(value.clone());
163                            }
164                        }
165                        ContextItem::ReferenceItem(_) => {
166                            // Skip ReferenceItems as they don't contain key-value pairs
167                        }
168                    }
169                }
170                None
171            }
172            RoCrateContext::ReferenceContext(_reference) => None,
173            RoCrateContext::EmbeddedContext(_context) => None,
174        }
175    }
176
177    pub fn add_urn_uuid(&mut self) {
178        match self {
179            RoCrateContext::ExtendedContext(context) => {
180                for x in context {
181                    match x {
182                        ContextItem::EmbeddedContext(embedded) => {
183                            let urn_found = embedded
184                                .get("@base")
185                                .is_some_and(|value| value.starts_with("urn:uuid:"));
186                            if !urn_found {
187                                embedded.insert(
188                                    "@base".to_string(),
189                                    format!("urn:uuid:{}", Uuid::now_v7()),
190                                );
191                            }
192                        }
193                        ContextItem::ReferenceItem(_) => {
194                            continue;
195                        }
196                    }
197                }
198            }
199            RoCrateContext::ReferenceContext(reference) => {
200                let mut base_map = HashMap::new();
201                base_map.insert("@base".to_string(), format!("urn:uuid:{}", Uuid::now_v7()));
202
203                *self = RoCrateContext::ExtendedContext(vec![
204                    ContextItem::ReferenceItem(reference.clone()),
205                    ContextItem::EmbeddedContext(base_map),
206                ]);
207            }
208            RoCrateContext::EmbeddedContext(context) => {
209                debug!("EmbeddedContext legacy of {:?}", context)
210            }
211        }
212    }
213
214    pub fn get_urn_uuid(&self) -> Option<String> {
215        let base = self.get_specific_context("@base");
216        base.map(|uuid| uuid.strip_prefix("urn:uuid:").unwrap().to_string())
217    }
218
219    /// Get the schema version from the context URLs
220    pub fn get_schema_version(&self) -> Option<schema::RoCrateSchemaVersion> {
221        self.get_all_context().iter().find_map(|context_url| {
222            if context_url.contains("ro/crate/1.1") {
223                Some(RoCrateSchemaVersion::V1_1)
224            } else if context_url.contains("ro/crate/1.2") {
225                Some(RoCrateSchemaVersion::V1_2)
226            } else {
227                None
228            }
229        })
230    }
231}
232
233#[cfg(test)]
234mod write_crate_tests {
235    use crate::ro_crate::read::read_crate;
236    use crate::ro_crate::schema;
237    use std::path::Path;
238    use std::path::PathBuf;
239
240    fn fixture_path(relative_path: &str) -> PathBuf {
241        Path::new("tests/fixtures").join(relative_path)
242    }
243
244    #[test]
245    fn test_get_all_context() {
246        let path = fixture_path("_ro-crate-metadata-complex-context.json");
247        let rocrate = read_crate(&path, 0).unwrap();
248
249        let mut context = rocrate.context.get_all_context();
250        context.sort();
251
252        let mut fixture_vec = vec![
253            "https://w3id.org/ro/crate/1.1/context",
254            "https://criminalcharacters.com/vocab#education",
255            "https://w3id.org/ro/terms/criminalcharacters#interests",
256        ];
257        fixture_vec.sort();
258
259        assert_eq!(context, fixture_vec);
260    }
261
262    #[test]
263    fn test_add_urn() {
264        let path = fixture_path("_ro-crate-metadata-complex-context.json");
265        let mut rocrate = read_crate(&path, 0).unwrap();
266
267        let mut context = rocrate.context.get_all_context();
268        assert_eq!(context.len(), 3);
269
270        rocrate.context.add_urn_uuid();
271        context = rocrate.context.get_all_context();
272
273        assert_eq!(context.len(), 4);
274    }
275
276    #[test]
277    fn test_get_context_from_key() {
278        let path = fixture_path("_ro-crate-metadata-complex-context.json");
279        let rocrate = read_crate(&path, 0).unwrap();
280
281        let specific_context = rocrate.context.get_specific_context("education");
282
283        assert_eq!(
284            specific_context.unwrap(),
285            "https://criminalcharacters.com/vocab#education"
286        );
287    }
288
289    #[test]
290    fn test_schema_version() {
291        let path = fixture_path("_ro-crate-metadata-complex-context.json");
292        let rocrate = read_crate(&path, 0).unwrap();
293
294        let schema_version = rocrate.context.get_schema_version();
295
296        assert_eq!(schema_version.unwrap(), schema::RoCrateSchemaVersion::V1_1);
297    }
298}