From 26fc5978d03abdcc679db55c84fbed04fa1b50cd Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Champin Date: Tue, 24 Oct 2023 13:52:16 +0200 Subject: [PATCH] sophia_resource now supports more formats --- resource/Cargo.toml | 9 ++++ resource/src/lib.rs | 18 +++++++ resource/src/loader/_local.rs | 15 +++++- resource/src/loader/_trait.rs | 32 +++++++++++- resource/src/loader/test.rs | 92 ++++++++++++++++++++++++++++++++++- resource/test/file3.nt | 20 ++++++++ resource/test/file4.jsonld | 88 +++++++++++++++++++++++++++++++++ resource/test/file5.rdf | 1 + sophia/Cargo.toml | 8 +-- 9 files changed, 276 insertions(+), 7 deletions(-) create mode 100644 resource/test/file3.nt create mode 100644 resource/test/file4.jsonld create mode 100644 resource/test/file5.rdf diff --git a/resource/Cargo.toml b/resource/Cargo.toml index 97fc64d9..27e28a74 100644 --- a/resource/Cargo.toml +++ b/resource/Cargo.toml @@ -13,11 +13,20 @@ keywords.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] +# This feature enables the JSON-LD parser and serializer +jsonld = ["sophia_jsonld"] +# This feature enables the RDF/XML parser and serializer +xml = ["sophia_xml"] +# This feature enables the HTTP client in dependencies +http_client = [] + [dependencies] sophia_api.workspace = true sophia_iri.workspace = true +sophia_jsonld = { workspace = true, optional = true } sophia_turtle.workspace = true +sophia_xml = { workspace = true, optional = true } thiserror.workspace = true [dev-dependencies] diff --git a/resource/src/lib.rs b/resource/src/lib.rs index 8ab16f3d..508961d5 100644 --- a/resource/src/lib.rs +++ b/resource/src/lib.rs @@ -33,10 +33,20 @@ mod test { pub const F2R1: Iri<&str> = Iri::new_unchecked_const("http://example.org/file2.ttl#res1"); pub const F2R2: Iri<&str> = Iri::new_unchecked_const("http://example.org/file2.ttl#res2"); pub const FAIL: Iri<&str> = Iri::new_unchecked_const("http://example.org/not_there"); + pub const F3: Iri<&str> = Iri::new_unchecked_const("http://example.org/file3.nt"); + #[cfg(feature = "jsonld")] + pub const F4: Iri<&str> = Iri::new_unchecked_const("http://example.org/file4.jsonld"); + #[cfg(feature = "xml")] + pub const F5: Iri<&str> = Iri::new_unchecked_const("http://example.org/file5.rdf"); pub const SUBDIR: Iri<&str> = Iri::new_unchecked_const("http://example.org/subdir"); // test with no extension (conneg emulation) pub const F1X: Iri<&str> = Iri::new_unchecked_const("http://example.org/file1"); pub const F1XR1: Iri<&str> = Iri::new_unchecked_const("http://example.org/file1#res1"); + pub const F3X: Iri<&str> = Iri::new_unchecked_const("http://example.org/file3"); + #[cfg(feature = "jsonld")] + pub const F4X: Iri<&str> = Iri::new_unchecked_const("http://example.org/file4"); + #[cfg(feature = "xml")] + pub const F5X: Iri<&str> = Iri::new_unchecked_const("http://example.org/file5"); pub const EX_ID: Iri<&str> = Iri::new_unchecked_const("http://example.org/ns#id"); pub const EX_LIST: Iri<&str> = Iri::new_unchecked_const("http://example.org/ns#list"); @@ -52,6 +62,14 @@ mod test { pub const F1_LEN: usize = 20; /// Number of triples in F2 pub const F2_LEN: usize = 2; + /// Number of triples in F3 + pub const F3_LEN: usize = 20; + /// Number of triples in F4 + #[cfg(feature = "jsonld")] + pub const F4_LEN: usize = 20; + /// Number of triples in F5 + #[cfg(feature = "xml")] + pub const F5_LEN: usize = 20; pub type MyGraph = Vec<[SimpleTerm<'static>; 3]>; pub type TestResult = Result<(), Box>; diff --git a/resource/src/loader/_local.rs b/resource/src/loader/_local.rs index dc37a85c..30b4c623 100644 --- a/resource/src/loader/_local.rs +++ b/resource/src/loader/_local.rs @@ -60,6 +60,12 @@ impl LocalLoader { fn ctype(&self, iri: &str) -> String { if iri.ends_with(".ttl") { "text/turtle".into() + } else if iri.ends_with(".nt") { + "application/n-triples".into() + } else if cfg!(feature = "jsonld") && iri.ends_with(".jsonld") { + "application/ld+json".into() + } else if cfg!(feature = "xml") && iri.ends_with(".rdf") { + "application/rdf+xml".into() } else { "application/octet-stream".into() } @@ -80,7 +86,14 @@ impl Loader for LocalLoader { // emulate conneg if there is no extension let no_ext = iri.as_bytes()[iri.rfind(['.', '/']).unwrap_or(0)] != b'.'; if no_ext { - for ext in ["ttl", "nt"] { + for ext in [ + "ttl", + "nt", + #[cfg(feature = "jsonld")] + "jsonld", + #[cfg(feature = "xml")] + "rdf", + ] { let alt = Iri::new_unchecked(format!("{}.{}", iri, ext)); if let Ok(res) = self.get(alt) { return Ok(res); diff --git a/resource/src/loader/_trait.rs b/resource/src/loader/_trait.rs index 1683a2d8..0724f000 100644 --- a/resource/src/loader/_trait.rs +++ b/resource/src/loader/_trait.rs @@ -5,7 +5,7 @@ use sophia_api::parser::TripleParser; use sophia_api::source::TripleSource; use sophia_api::term::Term; use sophia_iri::Iri; -use sophia_turtle::parser::turtle; +use sophia_turtle::parser::{nt, turtle}; use std::borrow::Borrow; use std::fmt::Debug; use std::io; @@ -40,6 +40,36 @@ pub trait Loader: Sized { .parse(bufread) .collect_triples() .map_err(|err| LoaderError::ParseError(iri_buf(iri_str), Box::new(err))), + + "application/n-triples" => nt::NTriplesParser {} + .parse(bufread) + .collect_triples() + .map_err(|err| LoaderError::ParseError(iri_buf(iri_str), Box::new(err))), + + #[cfg(feature = "jsonld")] + "application/ld+json" => { + use sophia_api::prelude::{Quad, QuadParser, QuadSource}; + use sophia_jsonld::{JsonLdOptions, JsonLdParser}; + let options = + JsonLdOptions::new().with_base(iri.as_ref().map_unchecked(|t| t.into())); + // TODO use this loader as the document loader for the JSON-LD parser + // (requires to provide an adaptater) + JsonLdParser::new_with_options(options) + .parse(bufread) + .filter_quads(|q| q.g().is_none()) + .map_quads(Quad::into_triple) + .collect_triples() + .map_err(|err| LoaderError::ParseError(iri_buf(iri_str), Box::new(err))) + } + + #[cfg(feature = "xml")] + "application/rdf+xml" => sophia_xml::parser::RdfXmlParser { + base: Some(iri.as_ref().map_unchecked(|t| t.borrow().to_string())), + } + .parse(bufread) + .collect_triples() + .map_err(|err| LoaderError::ParseError(iri_buf(iri_str), Box::new(err))), + _ => Err(LoaderError::CantGuessSyntax(iri_buf(iri_str))), } } diff --git a/resource/src/loader/test.rs b/resource/src/loader/test.rs index 4362ce2b..215104ae 100644 --- a/resource/src/loader/test.rs +++ b/resource/src/loader/test.rs @@ -48,6 +48,70 @@ fn get_file1_with_add() -> TestResult { Ok(()) } +#[test] +fn get_file3() -> TestResult { + let ldr = make_loader(); + assert_eq!( + ldr.get(F3)?, + (read("test/file3.nt")?, "application/n-triples".into()), + ); + Ok(()) +} + +#[test] +fn get_file3_no_ext() -> TestResult { + let ldr = make_loader(); + assert_eq!( + ldr.get(F3X)?, + (read("test/file3.nt")?, "application/n-triples".into()), + ); + Ok(()) +} + +#[cfg(feature = "jsonld")] +#[test] +fn get_file4() -> TestResult { + let ldr = make_loader(); + assert_eq!( + ldr.get(F4)?, + (read("test/file4.jsonld")?, "application/ld+json".into()), + ); + Ok(()) +} + +#[cfg(feature = "jsonld")] +#[test] +fn get_file4_no_ext() -> TestResult { + let ldr = make_loader(); + assert_eq!( + ldr.get(F4X)?, + (read("test/file4.jsonld")?, "application/ld+json".into()), + ); + Ok(()) +} + +#[cfg(feature = "xml")] +#[test] +fn get_file5() -> TestResult { + let ldr = make_loader(); + assert_eq!( + ldr.get(F5)?, + (read("test/file5.rdf")?, "application/rdf+xml".into()), + ); + Ok(()) +} + +#[cfg(feature = "xml")] +#[test] +fn get_file5_no_ext() -> TestResult { + let ldr = make_loader(); + assert_eq!( + ldr.get(F5X)?, + (read("test/file5.rdf")?, "application/rdf+xml".into()), + ); + Ok(()) +} + #[test] fn file_not_found() -> TestResult { let ldr = make_loader(); @@ -73,13 +137,39 @@ fn io_error() -> TestResult { } #[test] -fn graph() -> TestResult { +fn graph_from_ttl() -> TestResult { let ldr = make_loader(); let g: MyGraph = ldr.get_graph(F1)?; assert_eq!(g.len(), F1_LEN); Ok(()) } +#[test] +fn graph_from_nt() -> TestResult { + let ldr = make_loader(); + let g: MyGraph = ldr.get_graph(F3)?; + assert_eq!(g.len(), F3_LEN); + Ok(()) +} + +#[cfg(feature = "jsonld")] +#[test] +fn graph_from_jsonld() -> TestResult { + let ldr = make_loader(); + let g: MyGraph = ldr.get_graph(F4)?; + assert_eq!(g.len(), F4_LEN); + Ok(()) +} + +#[cfg(feature = "xml")] +#[test] +fn graph_from_rdfxml() -> TestResult { + let ldr = make_loader(); + let g: MyGraph = ldr.get_graph(F5)?; + assert_eq!(g.len(), F5_LEN); + Ok(()) +} + #[test] fn resource() -> TestResult { let ldr = make_loader().arced(); diff --git a/resource/test/file3.nt b/resource/test/file3.nt new file mode 100644 index 00000000..b8df37c6 --- /dev/null +++ b/resource/test/file3.nt @@ -0,0 +1,20 @@ + "res1". + . + . + . + _:b. + . + . + . +_:riog00000001 . +_:riog00000001 _:riog00000002. +_:riog00000002 . +_:riog00000002 _:riog00000003. +_:riog00000003 . +_:riog00000003 . + _:riog00000001. + "res2". + . + "res3". + . +_:b "res4". diff --git a/resource/test/file4.jsonld b/resource/test/file4.jsonld new file mode 100644 index 00000000..dd5b8925 --- /dev/null +++ b/resource/test/file4.jsonld @@ -0,0 +1,88 @@ +[ + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file1.ttl#res1", + "http://example.org/ns#next": [ + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file1.ttl#res2" + } + ], + "http://example.org/ns#id": [ + { + "@value": "res1" + } + ], + "http://example.org/ns#list": [ + { + "@list": [ + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file1.ttl#res3" + }, + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file1.ttl#res2" + }, + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file2.ttl#res1" + } + ] + } + ], + "http://example.org/ns#foreign1": [ + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file2.ttl#res1" + } + ], + "http://example.org/ns#related": [ + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file1.ttl#res2" + }, + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file1.ttl#res3" + }, + { "@id": "_:b" } + ], + "http://example.org/ns#foreign2": [ + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file2.ttl#res2" + } + ], + "http://example.org/ns#unreachable": [ + { + "@id": "http://somewhere.else/" + } + ] + }, + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file1.ttl#res2", + "http://example.org/ns#list": [ + { + "@list": [] + } + ], + "http://example.org/ns#id": [ + { + "@value": "res2" + } + ] + }, + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file1.ttl#res3", + "http://example.org/ns#id": [ + { + "@value": "res3" + } + ], + "http://example.org/ns#related": [ + { + "@id": "file:///home/pa/dev/sophia_rs/resource/test/file1.ttl#res2" + } + ] + }, + { + "@id": "_:b", + "http://example.org/ns#id": [ + { + "@value": "res4" + } + ] + } +] \ No newline at end of file diff --git a/resource/test/file5.rdf b/resource/test/file5.rdf new file mode 100644 index 00000000..f326ee6d --- /dev/null +++ b/resource/test/file5.rdf @@ -0,0 +1 @@ +res1res2res3res4 \ No newline at end of file diff --git a/sophia/Cargo.toml b/sophia/Cargo.toml index 0b465495..9b090ff6 100644 --- a/sophia/Cargo.toml +++ b/sophia/Cargo.toml @@ -16,13 +16,13 @@ all-features = true [features] default = [] # This feature enables the JSON-LD parser and serializer -jsonld = ["sophia_jsonld"] +jsonld = ["sophia_jsonld", "sophia_resource/jsonld"] # This feature enables the RDF/XML parser and serializer -xml = ["sophia_xml"] +xml = ["sophia_xml", "sophia_resource/xml"] # This feature enables to use the graph and dataset test macros in other crates test_macro = ["sophia_api/test_macro"] -# This feature enables the HTTP client crate in dependencies -http_client = ["sophia_jsonld/http_client"] +# This feature enables the HTTP client in dependencies +http_client = ["sophia_jsonld/http_client", "sophia_resource/http_client"] [dependencies] sophia_iri.workspace = true