Skip to content

Commit 85d7ca3

Browse files
authored
Merge pull request #6 from arunaengine/feat/remote-subcrate-main
Feat/remote subcrate handling
2 parents 3a635a3 + ddf0251 commit 85d7ca3

20 files changed

Lines changed: 2414 additions & 220 deletions

Cargo.lock

Lines changed: 221 additions & 164 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "ro-crate-rs"
33
version = "0.4.12"
4-
edition = "2021"
4+
edition = "2024"
55
repository = "https://github.com/intbio-ncl/ro-crate-rs"
66
authors = ["Matt Burridge <m.burridge1@newcastle.ac.uk>"]
77
autoexamples = false
@@ -31,6 +31,11 @@ dirs = "6.0.0"
3131
polars = { version = "0.52.0", features = ["parquet"] }
3232
log = "0.4.29"
3333
thiserror = "2.0.17"
34+
sha1 = "0.10.6"
35+
sha2 = "0.10.9"
36+
hex = "0.4.3"
37+
bytes = "1.11.0"
38+
regex = "1.12.2"
3439

3540
[lib]
3641
name = "rocraters"
@@ -39,6 +44,7 @@ doctest = false
3944

4045
[features]
4146
parquet = ["dep:polars"]
47+
subcrate-resolution = ["dep:sha1","dep:sha2","dep:hex","dep:bytes","dep:regex"]
4248

4349
[dependencies]
4450
serde = { workspace = true }
@@ -54,6 +60,13 @@ dirs = { workspace = true }
5460
polars = { workspace = true, optional = true }
5561
thiserror = { workspace = true }
5662
log = { workspace = true }
63+
sha1 = { workspace = true, optional = true }
64+
sha2 = { workspace = true, optional = true }
65+
hex = { workspace = true, optional = true }
66+
bytes = { workspace = true, optional = true }
67+
regex = { workspace = true, optional = true }
68+
5769

5870
[dev-dependencies]
5971
tempfile = "3.24"
72+
mockito = "1.7.1"

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
Working in Alpha
2-
31
# Overview
42

53
ro-crate-rs is a rust library, python library and CLI tool for interfacing with [Research Object Crates](https://www.researchobject.org/ro-crate/).
@@ -59,5 +57,5 @@ full python implementation.
5957

6058
# Compatability
6159

62-
RO-Crate v1.1 only
60+
RO-Crate v1.1 and v1.2
6361

benches/create_and_write.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use rocraters::ro_crate::constraints::*;
44
use rocraters::ro_crate::contextual_entity::ContextualEntity;
55
use rocraters::ro_crate::data_entity::DataEntity;
66
use rocraters::ro_crate::metadata_descriptor::MetadataDescriptor;
7-
use rocraters::ro_crate::read::{crate_path, read_crate, CrateReadError};
7+
use rocraters::ro_crate::read::{CrateReadError, crate_path, read_crate};
88
use rocraters::ro_crate::rocrate::GraphVector;
99
use rocraters::ro_crate::rocrate::{RoCrate, RoCrateContext};
1010
use rocraters::ro_crate::root::RootDataEntity;

cli/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ fn main() {
166166
println!("{}", to_string_pretty(&rocrate).unwrap());
167167
} else {
168168
match to_string_pretty(&rocrate) {
169-
Ok(json_ld) => {
169+
Ok(_json_ld) => {
170170
let mut table = json_to_table(&json!(&rocrate.graph)).into_table();
171171
table.with(Style::modern_rounded());
172172
if read_crate_command.fit {
@@ -275,7 +275,7 @@ fn main() {
275275
ValidateCommand::Basic(basic) => {
276276
let crate_name = crate_path(&basic.target_crate);
277277
match read_crate(&crate_name, 2) {
278-
Ok(rocrate) => println!("Crate Valid"),
278+
Ok(_rocrate) => println!("Crate Valid"),
279279
Err(e) => println!("Crate not valid: {:?}", e),
280280
}
281281
}

src/ro_crate/context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ impl RoCrateContext {
179179
RoCrateContext::ExtendedContext(context) => {
180180
for x in context {
181181
match x {
182-
ContextItem::EmbeddedContext(ref mut embedded) => {
182+
ContextItem::EmbeddedContext(embedded) => {
183183
let urn_found = embedded
184184
.get("@base")
185185
.is_some_and(|value| value.starts_with("urn:uuid:"));

src/ro_crate/contextual_entity.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
use crate::ro_crate::constraints::*;
77
use crate::ro_crate::modify::*;
88
use serde::{
9-
de::{self, MapAccess, Visitor},
109
Deserialize, Deserializer, Serialize, Serializer,
10+
de::{self, MapAccess, Visitor},
1111
};
1212
use std::collections::HashMap;
1313
use std::fmt;

src/ro_crate/convert.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,18 @@ use crate::ro_crate::graph_vector::GraphVector;
1414
use crate::ro_crate::rocrate::RoCrate;
1515
use log::warn;
1616
use polars::prelude::*;
17+
use thiserror::Error;
1718
use std::collections::HashMap;
1819
use std::path::PathBuf;
1920

21+
#[derive(Debug, Error)]
22+
pub enum ConvertError {
23+
#[error(transparent)]
24+
IoError(#[from] std::io::Error),
25+
#[error(transparent)]
26+
PolarsError(#[from] polars::error::PolarsError)
27+
}
28+
2029
pub fn to_df(rocrate: &RoCrate) -> DataFrame {
2130
// Get uuid
2231
let uuid = rocrate.context.get_urn_uuid().unwrap();
@@ -45,13 +54,15 @@ pub fn to_df(rocrate: &RoCrate) -> DataFrame {
4554
.unwrap()
4655
}
4756

48-
pub fn write_csv(df: &mut DataFrame, path: PathBuf) {
49-
let mut file = std::fs::File::create(path).unwrap();
50-
CsvWriter::new(&mut file).finish(df).unwrap();
57+
pub fn write_csv(df: &mut DataFrame, path: PathBuf) -> Result<(), ConvertError> {
58+
let mut file = std::fs::File::create(path)?;
59+
CsvWriter::new(&mut file).finish(df)?;
60+
Ok(())
5161
}
52-
pub fn write_parquet(df: &mut DataFrame, path: PathBuf) {
53-
let mut file = std::fs::File::create(path).unwrap();
54-
ParquetWriter::new(&mut file).finish(df).unwrap();
62+
pub fn write_parquet(df: &mut DataFrame, path: PathBuf) -> Result<(), ConvertError> {
63+
let mut file = std::fs::File::create(path)?;
64+
ParquetWriter::new(&mut file).finish(df)?;
65+
Ok(())
5566
}
5667

5768
pub fn to_parquet(df: &mut DataFrame) -> Result<u64, PolarsError> {
@@ -270,8 +281,8 @@ mod write_crate_tests {
270281
let mut df = to_df(&rocrate);
271282
let path_csv = fixture_path("test-ro-crate.csv");
272283
let path_parquet = fixture_path("test-ro-crate.parquet");
273-
write_csv(&mut df, path_csv);
274-
write_parquet(&mut df, path_parquet);
284+
write_csv(&mut df, path_csv).unwrap();
285+
write_parquet(&mut df, path_parquet).unwrap();
275286
}
276287

277288
#[test]
@@ -287,9 +298,9 @@ mod write_crate_tests {
287298
println!("Created dataframe");
288299
let path_csv = fixture_path("test-ro-crate.csv");
289300
let path_parquet = fixture_path("test-ro-crate.parquet");
290-
write_csv(&mut df, path_csv);
301+
write_csv(&mut df, path_csv).unwrap();
291302
println!("CSV created");
292-
write_parquet(&mut df, path_parquet);
303+
write_parquet(&mut df, path_parquet).unwrap();
293304
println!("Parquet created");
294305
}
295306
}

src/ro_crate/data_entity.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
use crate::ro_crate::constraints::*;
77
use crate::ro_crate::modify::*;
88
use serde::{
9-
de::{self, MapAccess, Visitor},
109
Deserialize, Deserializer, Serialize, Serializer,
10+
de::{self, MapAccess, Visitor},
1111
};
1212
use std::collections::HashMap;
1313
use std::fmt;

src/ro_crate/graph_vector.rs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ use serde::ser::Serializer;
1010
use serde::{Deserialize, Deserializer, Serialize};
1111
use serde_json::{Error as SerdeJsonError, Value};
1212
use std::collections::HashMap;
13-
use std::path::Path;
14-
use url::Url;
1513

1614
/// Represents the various types of entities contained within the graph of an RO-Crate.
1715
///
@@ -41,25 +39,25 @@ impl GraphVector {
4139
/// entity is correctly referenced in the root data entity's `hasPart` property if it is not already listed.
4240
pub fn overwrite(&mut self, new_data: GraphVector) -> bool {
4341
match self {
44-
GraphVector::DataEntity(ref mut existing_entity) => {
42+
GraphVector::DataEntity(existing_entity) => {
4543
if let GraphVector::DataEntity(new_entity) = new_data {
4644
*existing_entity = new_entity;
4745
return true;
4846
}
4947
}
50-
GraphVector::ContextualEntity(ref mut existing_entity) => {
48+
GraphVector::ContextualEntity(existing_entity) => {
5149
if let GraphVector::ContextualEntity(new_entity) = new_data {
5250
*existing_entity = new_entity;
5351
return true;
5452
}
5553
}
56-
GraphVector::RootDataEntity(ref mut existing_entity) => {
54+
GraphVector::RootDataEntity(existing_entity) => {
5755
if let GraphVector::RootDataEntity(new_entity) = new_data {
5856
*existing_entity = new_entity;
5957
return true;
6058
}
6159
}
62-
GraphVector::MetadataDescriptor(ref mut existing_entity) => {
60+
GraphVector::MetadataDescriptor(existing_entity) => {
6361
if let GraphVector::MetadataDescriptor(new_entity) = new_data {
6462
*existing_entity = new_entity;
6563
return true;
@@ -351,10 +349,14 @@ fn try_deserialize_into_graph_vector(value: &Value) -> Result<GraphVector, Serde
351349
}
352350
"./" => RootDataEntity::deserialize(value).map(GraphVector::RootDataEntity),
353351
_ => {
354-
if is_valid_url_or_path(id) {
355-
DataEntity::deserialize(value).map(GraphVector::DataEntity)
352+
if let Some(entity_type) = value.get("@type") {
353+
if is_data_entity(entity_type, id) {
354+
DataEntity::deserialize(value).map(GraphVector::DataEntity)
355+
} else {
356+
ContextualEntity::deserialize(value).map(GraphVector::ContextualEntity)
357+
}
356358
} else {
357-
ContextualEntity::deserialize(value).map(GraphVector::ContextualEntity)
359+
Err(serde::de::Error::custom("Missing or invalid '@type' field"))
358360
}
359361
}
360362
}
@@ -368,6 +370,32 @@ fn try_deserialize_into_graph_vector(value: &Value) -> Result<GraphVector, Serde
368370
/// This function is used as part of the custom deserialization process in `GraphVector`
369371
/// to distinguish between `DataEntity` and `ContextualEntity`. It validates whether the
370372
/// provided string (`s`) is a well-formed URL or a path that corresponds to an existing file.
371-
fn is_valid_url_or_path(s: &str) -> bool {
372-
Url::parse(s).is_ok() || Path::new(s).exists()
373+
///
374+
/// NOTE: The documentation states that:
375+
/// ```
376+
/// An entity which has File or Dataset as one of its @type values:
377+
///
378+
/// Is considered to be a Data Entity if its @id is an absolute URI or a relative URI.
379+
/// MAY have an @id which is a local identifier beginning with a #, in which case it is not considered to be a Data Entity.
380+
/// ```
381+
/// So the file path does not need to exist, and even if it exists, this library does not
382+
/// necessarily know the root of the crate. Also ContextualEntities can contain valid URIs as
383+
/// the documentation says [here](https://www.researchobject.org/ro-crate/specification/1.2/contextual-entities.html#identifiers-for-contextual-entities)
384+
// fn _is_valid_url_or_path(s: &str) -> bool {
385+
// Url::parse(s).is_ok() || Path::new(s).exists()
386+
// }
387+
fn is_data_entity(entity_type: &Value, id: &str) -> bool {
388+
if id.starts_with('#') {
389+
return false;
390+
}
391+
392+
entity_type.as_str().map_or_else(
393+
|| {
394+
entity_type.as_array().map_or(false, |arr| {
395+
arr.iter()
396+
.any(|v| matches!(v.as_str(), Some("File" | "Dataset")))
397+
})
398+
},
399+
|s| matches!(s, "File" | "Dataset"),
400+
)
373401
}

0 commit comments

Comments
 (0)