Skip to main content

entrenar/research/ro_crate/
entity.rs

1//! RO-Crate entity types and structures.
2
3use serde::{Deserialize, Serialize};
4use serde_json::json;
5use std::collections::HashMap;
6
7/// RO-Crate entity types
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum EntityType {
10    /// Root dataset
11    Dataset,
12    /// File entity
13    File,
14    /// Person
15    Person,
16    /// Organization
17    Organization,
18    /// Software application
19    SoftwareApplication,
20    /// Creative work (paper, etc.)
21    CreativeWork,
22    /// Action (workflow execution, etc.)
23    CreateAction,
24    /// Custom type
25    Custom(String),
26}
27
28impl std::fmt::Display for EntityType {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            Self::Dataset => write!(f, "Dataset"),
32            Self::File => write!(f, "File"),
33            Self::Person => write!(f, "Person"),
34            Self::Organization => write!(f, "Organization"),
35            Self::SoftwareApplication => write!(f, "SoftwareApplication"),
36            Self::CreativeWork => write!(f, "CreativeWork"),
37            Self::CreateAction => write!(f, "CreateAction"),
38            Self::Custom(t) => write!(f, "{t}"),
39        }
40    }
41}
42
43/// An entity in the RO-Crate
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct RoCrateEntity {
46    /// Entity ID (path or URL)
47    #[serde(rename = "@id")]
48    pub id: String,
49    /// Entity type
50    #[serde(rename = "@type")]
51    pub type_field: String,
52    /// Additional properties
53    #[serde(flatten)]
54    pub properties: HashMap<String, serde_json::Value>,
55}
56
57impl RoCrateEntity {
58    /// Create a new entity
59    pub fn new(id: impl Into<String>, entity_type: EntityType) -> Self {
60        Self { id: id.into(), type_field: entity_type.to_string(), properties: HashMap::new() }
61    }
62
63    /// Add a property
64    pub fn with_property(
65        mut self,
66        key: impl Into<String>,
67        value: impl Into<serde_json::Value>,
68    ) -> Self {
69        self.properties.insert(key.into(), value.into());
70        self
71    }
72
73    /// Add a name property
74    pub fn with_name(self, name: impl Into<String>) -> Self {
75        self.with_property("name", name.into())
76    }
77
78    /// Add a description
79    pub fn with_description(self, description: impl Into<String>) -> Self {
80        self.with_property("description", description.into())
81    }
82
83    /// Add a reference to another entity
84    pub fn with_reference(self, key: impl Into<String>, ref_id: impl Into<String>) -> Self {
85        self.with_property(key, json!({ "@id": ref_id.into() }))
86    }
87
88    /// Add multiple references
89    pub fn with_references(
90        self,
91        key: impl Into<String>,
92        ref_ids: impl IntoIterator<Item = impl Into<String>>,
93    ) -> Self {
94        let refs: Vec<serde_json::Value> =
95            ref_ids.into_iter().map(|id| json!({ "@id": id.into() })).collect();
96        self.with_property(key, refs)
97    }
98
99    /// Create the root dataset entity
100    pub fn root_dataset() -> Self {
101        Self::new("./", EntityType::Dataset)
102    }
103
104    /// Create a file entity
105    pub fn file(path: impl Into<String>) -> Self {
106        Self::new(path, EntityType::File)
107    }
108
109    /// Create a person entity
110    pub fn person(id: impl Into<String>, name: impl Into<String>) -> Self {
111        Self::new(id, EntityType::Person).with_name(name)
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_entity_type_display_dataset() {
121        assert_eq!(EntityType::Dataset.to_string(), "Dataset");
122    }
123
124    #[test]
125    fn test_entity_type_display_file() {
126        assert_eq!(EntityType::File.to_string(), "File");
127    }
128
129    #[test]
130    fn test_entity_type_display_person() {
131        assert_eq!(EntityType::Person.to_string(), "Person");
132    }
133
134    #[test]
135    fn test_entity_type_display_organization() {
136        assert_eq!(EntityType::Organization.to_string(), "Organization");
137    }
138
139    #[test]
140    fn test_entity_type_display_software_application() {
141        assert_eq!(EntityType::SoftwareApplication.to_string(), "SoftwareApplication");
142    }
143
144    #[test]
145    fn test_entity_type_display_creative_work() {
146        assert_eq!(EntityType::CreativeWork.to_string(), "CreativeWork");
147    }
148
149    #[test]
150    fn test_entity_type_display_create_action() {
151        assert_eq!(EntityType::CreateAction.to_string(), "CreateAction");
152    }
153
154    #[test]
155    fn test_entity_type_display_custom() {
156        let custom = EntityType::Custom("MyType".to_string());
157        assert_eq!(custom.to_string(), "MyType");
158    }
159
160    #[test]
161    fn test_entity_type_clone() {
162        let et = EntityType::Dataset;
163        let cloned = et.clone();
164        assert_eq!(et, cloned);
165    }
166
167    #[test]
168    fn test_entity_type_eq() {
169        assert_eq!(EntityType::File, EntityType::File);
170        assert_ne!(EntityType::File, EntityType::Person);
171    }
172
173    #[test]
174    fn test_ro_crate_entity_new() {
175        let entity = RoCrateEntity::new("test-id", EntityType::Dataset);
176        assert_eq!(entity.id, "test-id");
177        assert_eq!(entity.type_field, "Dataset");
178        assert!(entity.properties.is_empty());
179    }
180
181    #[test]
182    fn test_ro_crate_entity_with_property() {
183        let entity = RoCrateEntity::new("test", EntityType::File).with_property("size", 1024);
184        assert_eq!(entity.properties.get("size"), Some(&json!(1024)));
185    }
186
187    #[test]
188    fn test_ro_crate_entity_with_name() {
189        let entity = RoCrateEntity::new("test", EntityType::Dataset).with_name("My Dataset");
190        assert_eq!(entity.properties.get("name"), Some(&json!("My Dataset")));
191    }
192
193    #[test]
194    fn test_ro_crate_entity_with_description() {
195        let entity =
196            RoCrateEntity::new("test", EntityType::Dataset).with_description("A test dataset");
197        assert_eq!(entity.properties.get("description"), Some(&json!("A test dataset")));
198    }
199
200    #[test]
201    fn test_ro_crate_entity_with_reference() {
202        let entity =
203            RoCrateEntity::new("test", EntityType::File).with_reference("author", "#person1");
204        let expected = json!({ "@id": "#person1" });
205        assert_eq!(entity.properties.get("author"), Some(&expected));
206    }
207
208    #[test]
209    fn test_ro_crate_entity_with_references() {
210        let entity = RoCrateEntity::new("test", EntityType::Dataset)
211            .with_references("hasPart", vec!["file1.txt", "file2.txt"]);
212        let parts = entity.properties.get("hasPart").expect("key should exist");
213        assert!(parts.is_array());
214        let arr = parts.as_array().expect("operation should succeed");
215        assert_eq!(arr.len(), 2);
216    }
217
218    #[test]
219    fn test_ro_crate_entity_root_dataset() {
220        let entity = RoCrateEntity::root_dataset();
221        assert_eq!(entity.id, "./");
222        assert_eq!(entity.type_field, "Dataset");
223    }
224
225    #[test]
226    fn test_ro_crate_entity_file() {
227        let entity = RoCrateEntity::file("data/model.safetensors");
228        assert_eq!(entity.id, "data/model.safetensors");
229        assert_eq!(entity.type_field, "File");
230    }
231
232    #[test]
233    fn test_ro_crate_entity_person() {
234        let entity = RoCrateEntity::person("#alice", "Alice Smith");
235        assert_eq!(entity.id, "#alice");
236        assert_eq!(entity.type_field, "Person");
237        assert_eq!(entity.properties.get("name"), Some(&json!("Alice Smith")));
238    }
239
240    #[test]
241    fn test_ro_crate_entity_clone() {
242        let entity = RoCrateEntity::new("test", EntityType::File).with_name("test.txt");
243        let cloned = entity.clone();
244        assert_eq!(entity.id, cloned.id);
245        assert_eq!(entity.type_field, cloned.type_field);
246    }
247
248    #[test]
249    fn test_ro_crate_entity_serde() {
250        let entity = RoCrateEntity::new("test", EntityType::Dataset)
251            .with_name("Test Dataset")
252            .with_description("A test");
253
254        let json = serde_json::to_string(&entity).expect("JSON serialization should succeed");
255        let deserialized: RoCrateEntity =
256            serde_json::from_str(&json).expect("JSON deserialization should succeed");
257        assert_eq!(entity.id, deserialized.id);
258        assert_eq!(entity.type_field, deserialized.type_field);
259    }
260
261    #[test]
262    fn test_entity_type_serde() {
263        let et = EntityType::SoftwareApplication;
264        let json = serde_json::to_string(&et).expect("JSON serialization should succeed");
265        let deserialized: EntityType =
266            serde_json::from_str(&json).expect("JSON deserialization should succeed");
267        assert_eq!(et, deserialized);
268    }
269
270    #[test]
271    fn test_entity_type_debug() {
272        assert_eq!(format!("{:?}", EntityType::Dataset), "Dataset");
273        assert_eq!(format!("{:?}", EntityType::Custom("Foo".to_string())), "Custom(\"Foo\")");
274    }
275
276    #[test]
277    fn test_ro_crate_entity_chained_methods() {
278        let entity = RoCrateEntity::root_dataset()
279            .with_name("My Research Crate")
280            .with_description("Contains ML artifacts")
281            .with_reference("author", "#researcher")
282            .with_property("datePublished", "2024-01-15");
283
284        assert_eq!(entity.properties.len(), 4);
285        assert_eq!(entity.properties.get("name"), Some(&json!("My Research Crate")));
286    }
287}