From 2a7fae51c161c4eb888e71bb49d82793d06a3af2 Mon Sep 17 00:00:00 2001 From: Vincent Emonet Date: Sat, 30 Mar 2024 11:49:13 +0100 Subject: [PATCH] refactor: creating a new intro nanopub now fails if the ORCID or name of the introduced Profile are empty --- js/src/nanopub.rs | 6 ++--- lib/docs/docs/playground.html | 2 +- lib/docs/docs/python.md | 2 +- lib/docs/docs/rust.md | 2 +- lib/src/nanopub.rs | 44 ++++++++++++++--------------------- lib/src/profile.rs | 4 ++-- lib/src/utils.rs | 8 +++++++ lib/tests/nanopub_test.rs | 10 ++++++++ python/src/nanopub.rs | 2 +- python/tests/test_nanopub.py | 4 ++-- 10 files changed, 47 insertions(+), 37 deletions(-) diff --git a/js/src/nanopub.rs b/js/src/nanopub.rs index 4a29cd2..3a6c8ce 100644 --- a/js/src/nanopub.rs +++ b/js/src/nanopub.rs @@ -50,7 +50,7 @@ impl Nanopub { .map_err(|e| JsValue::from_str(&e.to_string())) } - // TODO: optional args https://docs.rs/wasm-bindgen-derive/latest/wasm_bindgen_derive/#optional-arguments + // NOTE: optional args docs https://docs.rs/wasm-bindgen-derive/latest/wasm_bindgen_derive/#optional-arguments #[wasm_bindgen] pub fn publish(self, profile: &OptionNpProfile, server_url: Option) -> Promise { // Handle null/undefined profile @@ -161,13 +161,13 @@ impl NpProfile { private_key: &str, orcid_id: &str, name: &str, - introduction_nanopub_uri: &str, + introduction_nanopub_uri: String, ) -> Result { RsNpProfile::new(private_key, orcid_id, name, Some(introduction_nanopub_uri)) .map(|profile: RsNpProfile| Self { profile }) .map_err(|e| JsValue::from_str(&e.to_string())) } - // TODO: create from profile.yml file + // TODO: create from profile.yml file? #[wasm_bindgen(js_name = toString)] pub fn to_string(&self) -> String { diff --git a/lib/docs/docs/playground.html b/lib/docs/docs/playground.html index fb025a3..36c3a4e 100644 --- a/lib/docs/docs/playground.html +++ b/lib/docs/docs/playground.html @@ -115,7 +115,7 @@

āœļø Nanopublication signing playground šŸ•¹ import init, { Nanopub, NpProfile, KeyPair, getNpServer } from "https://unpkg.com/@nanopub/sign"; // import init, { Nanopub, NpProfile, KeyPair, getNpServer } from "./assets/pkg/web.js"; - // TODO: get ORCID based on the public key registered in the nanopub network + // TODO: get ORCID based on the public key registered in the nanopub network? let orcidToken = null; let orcid = ""; let privKey = null; diff --git a/lib/docs/docs/python.md b/lib/docs/docs/python.md index bcede35..f92b25f 100644 --- a/lib/docs/docs/python.md +++ b/lib/docs/docs/python.md @@ -150,7 +150,7 @@ keypair = KeyPair() new_profile = NpProfile( private_key=keypair.private, orcid_id="https://orcid.org/0000-0000-0000-0000", - name="", + name="Your Name", introduction_nanopub_uri="" ) diff --git a/lib/docs/docs/rust.md b/lib/docs/docs/rust.md index 65fd3c6..724d242 100644 --- a/lib/docs/docs/rust.md +++ b/lib/docs/docs/rust.md @@ -140,7 +140,7 @@ use tokio::runtime; let (private_key, _pubkey) = gen_keys().unwrap(); // Create a profile with this new private key -let profile = NpProfile::new(&private_key, "https://orcid.org/0000-0000-0000-0000", "", None).unwrap(); +let profile = NpProfile::new(&private_key, "https://orcid.org/0000-0000-0000-0000", "Your Name", None).unwrap(); // Publish a nanopub introduction for this profile let rt = runtime::Runtime::new().expect("Runtime failed"); diff --git a/lib/src/nanopub.rs b/lib/src/nanopub.rs index 9982f8e..e1b2210 100644 --- a/lib/src/nanopub.rs +++ b/lib/src/nanopub.rs @@ -13,8 +13,7 @@ use rsa::pkcs8::DecodePublicKey; use rsa::{sha2::Digest, sha2::Sha256, Pkcs1v15Sign, RsaPublicKey}; use sophia::api::dataset::{Dataset, MutableDataset}; use sophia::api::ns::{rdf, xsd, Namespace}; -use sophia::api::term::matcher::Any; -use sophia::api::term::{SimpleTerm, Term}; +use sophia::api::term::{matcher::Any, Term}; use sophia::inmem::dataset::LightDataset; use sophia::iri::{AsIriRef, Iri}; use std::collections::HashSet; @@ -40,7 +39,7 @@ impl RdfSource for &String { } } -/// A nanopublication object +/// A Nanopublication, contains the nanopub info (graphs URIs, signature, etc), and the RDF dataset. #[derive(Clone, Debug)] pub struct Nanopub { pub info: NpInfo, @@ -62,7 +61,6 @@ impl fmt::Display for Nanopub { } impl Nanopub { - // // TODO: change approach to use Nanopub::new(rdf).sign()? pub fn new(rdf: T) -> Result { let dataset = rdf.get_dataset()?; let np_info = extract_np_info(&dataset)?; @@ -122,7 +120,6 @@ impl Nanopub { /// ``` pub fn check(self) -> Result { let _ = self.is_valid()?; - let mut msg: String = "".to_string(); if self.info.trusty_hash.is_empty() { msg = format!("{}1 valid (not trusty)", msg); @@ -140,8 +137,8 @@ impl Nanopub { msg = format!("{}1 trusty", msg); } + // Check the signature is valid if found let mut unsigned_dataset = self.dataset.clone(); - // Check signature if found if !self.info.signature.is_empty() { // Remove the signature from the graph before re-generating it unsigned_dataset.remove( @@ -180,11 +177,12 @@ impl Nanopub { BOLD, self.info.uri, END, msg ); // let rdf = serialize_rdf(&self.dataset, &self.info.uri.as_ref(), &self.info.ns.as_ref())?; - // TODO: check if the np has been published with Nanopub::fetch + // TODO: should check return a string or a Nanopub? A string is not easy to process by machines + // Should we check if the np has been published with Nanopub::fetch? Ok(self) } - /// Sign an unsigned nanopub RDF and add trusty URI + /// Sign a nanopub: generate and add signature and trusty URI. If the nanopub is already signed, unsign it first. /// /// # Arguments /// @@ -249,13 +247,10 @@ impl Nanopub { { let now = Utc::now(); let datetime_str = now.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); - // TODO: there is an error when trying to cast the string to xsd::dateTime - // let lit_date = "2019" * xsd::dateTime; self.dataset.insert( self.info.ns.as_iri_ref(), ns("dct").get("created")?, - SimpleTerm::LiteralDatatype(datetime_str.into(), xsd::dateTime.iriref()), - // datetime_str.as_str() * xsd::dateTime, + datetime_str.as_str() * xsd::dateTime, Some(&self.info.pubinfo), )?; } @@ -334,7 +329,7 @@ impl Nanopub { Ok(self) } - /// Async function to sign and publish RDF to a nanopub server + /// Publish a nanopub to a nanopub server. If the nanopub is not signed and a profile is provided, it will be signed before publishing. /// /// # Arguments /// @@ -365,23 +360,19 @@ impl Nanopub { server_url: Option<&str>, ) -> Result { self = if let Some(profile) = profile { - // if !self.info.signature.is_empty() { - // // TODO: handle when the user provide a Profile and a signed Nanopub. - // // We should re-sign the Nanopub with the new profile - // return Err(NpError("Profile provided to sign an already signed Nanopub. Re-signing an already signed Nanopub is not supported yet".to_string())); - // } - // println!("Profile provided, signing Nanopub before publishing"); + // If profile provided we sign the nanopub self.sign(profile)? } else if self.info.signature.is_empty() { + // If no profile and nanopub not signed we throw an error return Err(NpError(format!( "No profile provided and nanopub not signed, could not sign the Nanopublication \n{}", self ))); } else { - // If the nanopub is already signed we verify it, then publish it + // If no profile provided, but the nanopub is already signed, we verify it, then publish it self.check()? }; - // Use test server if None provided + // Use test server if server_url not provided let server_url = if let Some(server_url) = server_url { if server_url.is_empty() { TEST_SERVER.to_string() @@ -403,7 +394,6 @@ impl Nanopub { // BOLD, self.info.published, END // ); } else { - println!("\nāŒ Issue publishing the Nanopublication \n{}", self); return Err(NpError(format!( "Issue publishing the Nanopublication \n{}", self @@ -448,10 +438,6 @@ impl Nanopub { self.info.uri = Iri::new_unchecked(NP_TEMP_URI.to_string()); self.info.ns = Namespace::new_unchecked(NP_TEMP_URI.to_string()); self.info = extract_np_info(&self.dataset)?; - // self.info.published = None; - // self.info.trusty_hash = "".to_string(); - // self.info.signature = "".to_string(); - // self.info.public_key = "".to_string(); Ok(self) } @@ -479,6 +465,12 @@ impl Nanopub { /// }); /// ``` pub fn new_intro(profile: &NpProfile) -> Result { + if profile.orcid_id.is_empty() { + return Err(NpError("Invalid Profile: ORCID is empty.".to_string())); + } + if profile.name.is_empty() { + return Err(NpError("Invalid Profile: name is empty.".to_string())); + } let mut dataset = create_base_dataset()?; let np_ns = Namespace::new_unchecked(NP_TEMP_URI); let assertion_graph = np_ns.get("assertion")?; diff --git a/lib/src/profile.rs b/lib/src/profile.rs index 612bb53..238d3c5 100644 --- a/lib/src/profile.rs +++ b/lib/src/profile.rs @@ -27,7 +27,7 @@ impl NpProfile { private_key: &str, orcid_id: &str, name: &str, - introduction_nanopub_uri: Option<&str>, + introduction_nanopub_uri: Option, ) -> Result { let privkey = RsaPrivateKey::from_pkcs8_der(&engine::general_purpose::STANDARD.decode(private_key)?)?; @@ -37,7 +37,7 @@ impl NpProfile { name: name.to_string(), public_key: get_pubkey_str(&pubkey)?, private_key: private_key.to_string(), - introduction_nanopub_uri: Some(introduction_nanopub_uri.unwrap_or("").to_string()), + introduction_nanopub_uri, }) } diff --git a/lib/src/utils.rs b/lib/src/utils.rs index 9ee77b5..f3cebd4 100644 --- a/lib/src/utils.rs +++ b/lib/src/utils.rs @@ -19,10 +19,18 @@ pub fn parse_rdf(rdf: &str) -> Result { let dataset = if rdf.trim().starts_with('{') || rdf.trim().starts_with('[') { parse_jsonld(&rdf)? } else { + // TODO: extract prefixes https://github.com/pchampin/sophia_rs/issues/45 // The TriG parser handles nquads trig::parse_str(&rdf) .collect_quads() .map_err(|e| NpError(format!("Error parsing TriG: {e}")))? + // NOTE: we can access the trig parser prefixes, but we always get an empty map, because it's not parsed yet + // let parser = trig::parse_str(&rdf); + // let prefixes = parser.0.prefixes(); + // println!("PREFIXES: {:?}", prefixes); + // let dataset = parser.collect_quads() + // .map_err(|e| NpError(format!("Error parsing TriG: {e}")))?; + // dataset }; Ok(dataset) } diff --git a/lib/tests/nanopub_test.rs b/lib/tests/nanopub_test.rs index 0871ef2..2150987 100644 --- a/lib/tests/nanopub_test.rs +++ b/lib/tests/nanopub_test.rs @@ -163,6 +163,16 @@ async fn publish_np_intro() -> Result<(), Box> { .await?; // println!("{}", np); assert!(np.info.published.is_some()); + // Test error when Profile not complete + let profile = NpProfile::new( + &get_test_key(), + "https://orcid.org/0000-0000-0000-0000", + "", + None, + )?; + assert!(Nanopub::new_intro(&profile).is_err()); + let profile = NpProfile::new(&get_test_key(), "", "Test User", None)?; + assert!(Nanopub::new_intro(&profile).is_err()); Ok(()) } diff --git a/python/src/nanopub.rs b/python/src/nanopub.rs index 595f3b3..0fe65b1 100644 --- a/python/src/nanopub.rs +++ b/python/src/nanopub.rs @@ -203,7 +203,7 @@ impl NpProfilePy { private_key: &str, orcid_id: &str, name: &str, - introduction_nanopub_uri: Option<&str>, + introduction_nanopub_uri: Option, ) -> PyResult { NpProfile::new(private_key, orcid_id, name, introduction_nanopub_uri) .map(|profile| Self { profile }) diff --git a/python/tests/test_nanopub.py b/python/tests/test_nanopub.py index c92c0be..04e76cc 100644 --- a/python/tests/test_nanopub.py +++ b/python/tests/test_nanopub.py @@ -37,7 +37,7 @@ profile = NpProfile( private_key=private_key, orcid_id="https://orcid.org/0000-0000-0000-0000", - name="", + name="Your Name", introduction_nanopub_uri="" ) @@ -68,7 +68,7 @@ def test_publish_intro(): new_profile = NpProfile( private_key=keypair.private, orcid_id="https://orcid.org/0000-0000-0000-0000", - name="", + name="Your Name", introduction_nanopub_uri="" ) np = Nanopub.publish_intro(new_profile)