From 11fdff350e0549d46a8a7b5e74451e08ce067cb2 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Tue, 12 Oct 2021 19:56:08 -0700 Subject: rust: implement scheman and API changes --- rust/src/editing_crud.rs | 17 ++++ rust/src/endpoint_handlers.rs | 194 +++++++++++++++++++++++++++++++++++------- rust/src/endpoints.rs | 71 ++++++++++++++-- rust/src/entity_crud.rs | 34 ++++++++ rust/src/identifiers.rs | 75 +++++++++++++++- 5 files changed, 353 insertions(+), 38 deletions(-) diff --git a/rust/src/editing_crud.rs b/rust/src/editing_crud.rs index 8da3cabd..fa39fdfb 100644 --- a/rust/src/editing_crud.rs +++ b/rust/src/editing_crud.rs @@ -28,6 +28,7 @@ use uuid::Uuid; pub trait EditorCrud { fn db_get(conn: &DbConn, editor_id: FatcatId) -> Result; + fn db_get_username(conn: &DbConn, username: &str) -> Result; fn db_create(&self, conn: &DbConn) -> Result; fn db_update_username(&self, conn: &DbConn, editor_id: FatcatId) -> Result; } @@ -45,6 +46,22 @@ impl EditorCrud for Editor { Ok(editor) } + fn db_get_username(conn: &DbConn, username: &str) -> Result { + let editor: EditorRow = match editor::table + .filter(editor::username.eq(username)) + .get_result(conn) + { + Ok(ed) => ed, + Err(diesel::result::Error::NotFound) => { + return Err( + FatcatError::NotFound("editor".to_string(), username.to_string()).into(), + ); + } + other => other?, + }; + Ok(editor) + } + fn db_create(&self, conn: &DbConn) -> Result { identifiers::check_username(&self.username)?; let is_admin = self.is_admin.unwrap_or(false); diff --git a/rust/src/endpoint_handlers.rs b/rust/src/endpoint_handlers.rs index 141c414e..9a76652b 100644 --- a/rust/src/endpoint_handlers.rs +++ b/rust/src/endpoint_handlers.rs @@ -104,35 +104,71 @@ impl Server { &self, conn: &DbConn, issnl: &Option, + issne: &Option, + issnp: &Option, + issn: &Option, wikidata_qid: &Option, expand_flags: ExpandFlags, hide_flags: HideFlags, ) -> Result { - let (ident, rev): (ContainerIdentRow, ContainerRevRow) = match (issnl, wikidata_qid) { - (Some(issnl), None) => { - check_issn(issnl)?; - container_ident::table - .inner_join(container_rev::table) - .filter(container_rev::issnl.eq(&issnl)) - .filter(container_ident::is_live.eq(true)) - .filter(container_ident::redirect_id.is_null()) - .first(conn)? - } - (None, Some(wikidata_qid)) => { - check_wikidata_qid(wikidata_qid)?; - container_ident::table - .inner_join(container_rev::table) - .filter(container_rev::wikidata_qid.eq(&wikidata_qid)) - .filter(container_ident::is_live.eq(true)) - .filter(container_ident::redirect_id.is_null()) - .first(conn)? - } - _ => { - return Err( - FatcatError::MissingOrMultipleExternalId("in lookup".to_string()).into(), - ); - } - }; + let (ident, rev): (ContainerIdentRow, ContainerRevRow) = + match (issnl, issnp, issne, issn, wikidata_qid) { + (Some(issnl), None, None, None, None) => { + check_issn(issnl)?; + container_ident::table + .inner_join(container_rev::table) + .filter(container_rev::issnl.eq(&issnl)) + .filter(container_ident::is_live.eq(true)) + .filter(container_ident::redirect_id.is_null()) + .first(conn)? + } + (None, Some(issnp), None, None, None) => { + check_issn(issnp)?; + container_ident::table + .inner_join(container_rev::table) + .filter(container_rev::issnp.eq(&issnp)) + .filter(container_ident::is_live.eq(true)) + .filter(container_ident::redirect_id.is_null()) + .first(conn)? + } + (None, None, Some(issne), None, None) => { + check_issn(issne)?; + container_ident::table + .inner_join(container_rev::table) + .filter(container_rev::issne.eq(&issne)) + .filter(container_ident::is_live.eq(true)) + .filter(container_ident::redirect_id.is_null()) + .first(conn)? + } + (None, None, None, Some(issn), None) => { + check_issn(issn)?; + container_ident::table + .inner_join(container_rev::table) + .filter( + container_rev::issnl + .eq(&issn) + .or(container_rev::issnp.eq(&issn)) + .or(container_rev::issne.eq(&issn)), + ) + .filter(container_ident::is_live.eq(true)) + .filter(container_ident::redirect_id.is_null()) + .first(conn)? + } + (None, None, None, None, Some(wikidata_qid)) => { + check_wikidata_qid(wikidata_qid)?; + container_ident::table + .inner_join(container_rev::table) + .filter(container_rev::wikidata_qid.eq(&wikidata_qid)) + .filter(container_ident::is_live.eq(true)) + .filter(container_ident::redirect_id.is_null()) + .first(conn)? + } + _ => { + return Err( + FatcatError::MissingOrMultipleExternalId("in lookup".to_string()).into(), + ); + } + }; let mut entity = ContainerEntity::db_from_row(conn, rev, Some(ident), hide_flags)?; entity.db_expand(&conn, expand_flags)?; @@ -264,6 +300,7 @@ impl Server { doaj: &Option, dblp: &Option, oai: &Option, + hdl: &Option, expand_flags: ExpandFlags, hide_flags: HideFlags, ) -> Result { @@ -281,8 +318,24 @@ impl Server { doaj, dblp, oai, + hdl, ) { - (Some(doi), None, None, None, None, None, None, None, None, None, None, None, None) => { + ( + Some(doi), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ) => { // DOIs always stored lower-case; lookups are case-insensitive let doi = doi.to_lowercase(); check_doi(&doi)?; @@ -307,6 +360,7 @@ impl Server { None, None, None, + None, ) => { check_wikidata_qid(wikidata_qid)?; release_ident::table @@ -330,6 +384,7 @@ impl Server { None, None, None, + None, ) => { check_isbn13(isbn13)?; let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) = @@ -357,6 +412,7 @@ impl Server { None, None, None, + None, ) => { check_pmid(pmid)?; release_ident::table @@ -380,6 +436,7 @@ impl Server { None, None, None, + None, ) => { check_pmcid(pmcid)?; release_ident::table @@ -403,6 +460,7 @@ impl Server { None, None, None, + None, ) => { check_core_id(core)?; release_ident::table @@ -426,6 +484,7 @@ impl Server { None, None, None, + None, ) => { // TODO: this allows only lookup by full, versioned arxiv identifier. Probably also // want to allow lookup by "work" style identifier? @@ -455,6 +514,7 @@ impl Server { None, None, None, + None, ) => { check_jstor_id(jstor)?; let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) = @@ -468,7 +528,22 @@ impl Server { .first(conn)?; (ident, rev) } - (None, None, None, None, None, None, None, None, Some(ark), None, None, None, None) => { + ( + None, + None, + None, + None, + None, + None, + None, + None, + Some(ark), + None, + None, + None, + None, + None, + ) => { check_ark_id(ark)?; let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) = release_rev::table @@ -481,7 +556,22 @@ impl Server { .first(conn)?; (ident, rev) } - (None, None, None, None, None, None, None, None, None, Some(mag), None, None, None) => { + ( + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(mag), + None, + None, + None, + None, + ) => { check_mag_id(mag)?; let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) = release_rev::table @@ -508,6 +598,7 @@ impl Server { Some(doaj), None, None, + None, ) => { check_doaj_id(doaj)?; let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) = @@ -535,6 +626,7 @@ impl Server { None, Some(dblp), None, + None, ) => { check_dblp_key(dblp)?; let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) = @@ -548,7 +640,22 @@ impl Server { .first(conn)?; (ident, rev) } - (None, None, None, None, None, None, None, None, None, None, None, None, Some(oai)) => { + ( + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(oai), + None, + ) => { check_oai_id(oai)?; let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) = release_rev::table @@ -561,6 +668,35 @@ impl Server { .first(conn)?; (ident, rev) } + ( + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(hdl), + ) => { + let hdl = hdl.to_lowercase(); + check_hdl(&hdl)?; + let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) = + release_rev::table + .inner_join(release_ident::table) + .inner_join(release_rev_extid::table) + .filter(release_rev_extid::extid_type.eq("hdl".to_string())) + .filter(release_rev_extid::value.eq(hdl)) + .filter(release_ident::is_live.eq(true)) + .filter(release_ident::redirect_id.is_null()) + .first(conn)?; + (ident, rev) + } _ => { return Err( FatcatError::MissingOrMultipleExternalId("in lookup".to_string()).into(), diff --git a/rust/src/endpoints.rs b/rust/src/endpoints.rs index 0dd232c6..e04e7315 100644 --- a/rust/src/endpoints.rs +++ b/rust/src/endpoints.rs @@ -8,7 +8,7 @@ #![allow(clippy::redundant_closure_call)] use crate::auth::FatcatRole; -use crate::database_models::EntityEditRow; +use crate::database_models::{EditorRow, EntityEditRow}; use crate::editing::*; use crate::editing_crud::{EditgroupAnnotationCrud, EditgroupCrud, EditorCrud}; use crate::entity_crud::{EntityCrud, ExpandFlags, HideFlags}; @@ -656,12 +656,6 @@ impl Api for Server { WorkEntity ); - wrap_lookup_handler!( - lookup_container, - lookup_container_handler, - LookupContainerResponse, - issnl - ); wrap_lookup_handler!( lookup_creator, lookup_creator_handler, @@ -696,6 +690,46 @@ impl Api for Server { ); wrap_fcid_handler!(get_editor, get_editor_handler, GetEditorResponse); + fn lookup_container( + &self, + issnl: Option, + issne: Option, + issnp: Option, + issn: Option, + wikidata_qid: Option, + expand: Option, + hide: Option, + _context: &Context, + ) -> Box + Send> { + let conn = self.db_pool.get().expect("db_pool error"); + let expand_flags = match expand { + None => ExpandFlags::none(), + Some(param) => ExpandFlags::from_str(¶m).unwrap(), + }; + let hide_flags = match hide { + None => HideFlags::none(), + Some(param) => HideFlags::from_str(¶m).unwrap(), + }; + // No transaction for GET + let ret = match self + .lookup_container_handler( + &conn, + &issnl, + &issne, + &issnp, + &issn, + &wikidata_qid, + expand_flags, + hide_flags, + ) + .map_err(|e| FatcatError::from(e)) + { + Ok(entity) => LookupContainerResponse::FoundEntity(entity), + Err(fe) => generic_err_responses!(fe, LookupContainerResponse), + }; + Box::new(futures::done(Ok(ret))) + } + fn lookup_file( &self, md5: Option, @@ -740,6 +774,7 @@ impl Api for Server { doaj: Option, dblp: Option, oai: Option, + hdl: Option, expand: Option, hide: Option, _context: &Context, @@ -770,6 +805,7 @@ impl Api for Server { &doaj, &dblp, &oai, + &hdl, expand_flags, hide_flags, ) @@ -882,6 +918,27 @@ impl Api for Server { Box::new(futures::done(Ok(ret))) } + fn lookup_editor( + &self, + username: Option, + _context: &Context, + ) -> Box + Send> { + let conn = self.db_pool.get().expect("db_pool error"); + let ret = match conn.transaction(|| match username { + Some(username) => { + let row: EditorRow = Editor::db_get_username(&conn, &username)?; + Ok(row.into_model()) + } + None => Err(FatcatError::MissingOrMultipleExternalId( + "in lookup".to_string(), + )), + }) { + Ok(editor) => LookupEditorResponse::Found(editor), + Err(fe) => generic_err_responses!(fe, LookupEditorResponse), + }; + Box::new(futures::done(Ok(ret))) + } + fn accept_editgroup( &self, editgroup_id: String, diff --git a/rust/src/entity_crud.rs b/rust/src/entity_crud.rs index b3b97c73..cbf9592b 100644 --- a/rust/src/entity_crud.rs +++ b/rust/src/entity_crud.rs @@ -797,10 +797,13 @@ impl EntityCrud for ContainerEntity { Ok(ContainerEntity { issnl: None, + issne: None, + issnp: None, wikidata_qid: None, publisher: None, name: None, container_type: None, + publication_status: None, state: Some(ident_row.state().unwrap().shortname()), ident: Some(FatcatId::from_uuid(&ident_row.id).to_string()), revision: ident_row.rev_id.map(|u| u.to_string()), @@ -829,10 +832,13 @@ impl EntityCrud for ContainerEntity { Ok(ContainerEntity { issnl: rev_row.issnl, + issne: rev_row.issne, + issnp: rev_row.issnp, wikidata_qid: rev_row.wikidata_qid, publisher: rev_row.publisher, name: Some(rev_row.name), container_type: rev_row.container_type, + publication_status: rev_row.publication_status, state, ident: ident_id, revision: Some(rev_row.id.to_string()), @@ -851,6 +857,16 @@ impl EntityCrud for ContainerEntity { if let Some(ref extid) = entity.issnl { check_issn(extid)?; } + if let Some(ref extid) = entity.issne { + check_issn(extid)?; + } + if let Some(ref extid) = entity.issnp { + check_issn(extid)?; + } + + if let Some(ref status) = entity.publication_status { + check_publication_status(status)?; + } } if models.iter().any(|m| m.name.is_none()) { @@ -868,8 +884,11 @@ impl EntityCrud for ContainerEntity { name: model.name.clone().unwrap(), // unwrap checked above publisher: model.publisher.clone(), issnl: model.issnl.clone(), + issnp: model.issnp.clone(), + issne: model.issne.clone(), wikidata_qid: model.wikidata_qid.clone(), container_type: model.container_type.clone(), + publication_status: model.publication_status.clone(), extra_json: model.extra.clone(), }) .collect::>(), @@ -1292,6 +1311,7 @@ impl EntityCrud for FilesetEntity { md5: r.md5, sha1: r.sha1, sha256: r.sha256, + mimetype: r.mimetype, extra: r.extra_json, }) .collect(); @@ -1374,6 +1394,7 @@ impl EntityCrud for FilesetEntity { md5: f.md5.clone(), sha1: f.sha1.clone(), sha256: f.sha256.clone(), + mimetype: f.mimetype.clone(), extra_json: f.extra.clone(), }) .collect(); @@ -1747,6 +1768,7 @@ impl EntityCrud for ReleaseEntity { doaj: None, dblp: None, oai: None, + hdl: None, }, refs: None, contribs: None, @@ -2026,6 +2048,7 @@ impl EntityCrud for ReleaseEntity { doaj: None, dblp: None, oai: None, + hdl: None, }; let extid_rows: Vec = release_rev_extid::table @@ -2041,6 +2064,7 @@ impl EntityCrud for ReleaseEntity { "doaj" => ext_ids.doaj = Some(extid_row.value), "dblp" => ext_ids.dblp = Some(extid_row.value), "oai" => ext_ids.oai = Some(extid_row.value), + "hdl" => ext_ids.hdl = Some(extid_row.value), _ => (), } } @@ -2128,6 +2152,9 @@ impl EntityCrud for ReleaseEntity { if let Some(ref extid) = entity.ext_ids.oai { check_oai_id(extid)?; } + if let Some(ref extid) = entity.ext_ids.hdl { + check_hdl(extid)?; + } if let Some(ref release_type) = entity.release_type { check_release_type(release_type)?; @@ -2335,6 +2362,13 @@ impl EntityCrud for ReleaseEntity { value: extid.clone(), }); }; + if let Some(extid) = &model.ext_ids.hdl { + release_extid_rows.push(ReleaseExtidRow { + release_rev: *rev_id, + extid_type: "hdl".to_string(), + value: extid.to_lowercase(), + }); + }; } for (model, rev_id) in models.iter().zip(rev_ids.iter()) { diff --git a/rust/src/identifiers.rs b/rust/src/identifiers.rs index 9d3734c9..3f5fc2cf 100644 --- a/rust/src/identifiers.rs +++ b/rust/src/identifiers.rs @@ -445,8 +445,8 @@ fn test_check_oai_id() { assert!(check_oai_id("oai:wibble.org:ab cd").is_err()); // space not permitted (must be escaped as %20) assert!(check_oai_id("oai:wibble.org:ab#cd").is_err()); // # not permitted assert!(check_oai_id("oai:wibble.org:ab Result<()> { + // currently strict about only allowing a fixed set of prefixes + // should explicitly not allow DOIs, even though DOIs are themselves handles + lazy_static! { + static ref RE: Regex = Regex::new(r"^(20|11|21|84).\d{1,6}(.\d{1,6})?/\S+$").unwrap(); + } + if raw.is_ascii() && RE.is_match(raw) { + Ok(()) + } else { + Err(FatcatError::MalformedExternalId( + "Handle (expected, eg, '20.500.23456/ABC/trs12')".to_string(), + raw.to_string(), + ))? + } +} + +#[test] +fn test_check_hdl() { + assert!(check_hdl("20.500.23456/ABC/DUMMY").is_ok()); + assert!(check_hdl("20.500.12690/RIN/IDDOAH/BTNH25").is_ok()); + assert!(check_hdl("20.500.12690/rin/iddoah/btnh25").is_ok()); + assert!(check_hdl("20.1234/aksjdfh").is_ok()); + assert!(check_hdl("21.1234/aksjdfh").is_ok()); + assert!(check_hdl("11.1234/aksjdfh").is_ok()); + assert!(check_hdl("20.500.23456/ABC/trs12").is_ok()); + assert!(check_hdl("20.500/ABC/trs12").is_ok()); + + assert!(check_hdl("10.1234/aksjdfh").is_err()); + assert!(check_hdl("0.1234/aksjdfh").is_err()); + assert!(check_hdl("20.1234/ßs").is_err()); + assert!(check_hdl("20.1234/aksjdfh ").is_err()); + assert!(check_hdl("20.1234/ak sjdfh").is_err()); + assert!(check_hdl("20.1234/aks\tjdfh").is_err()); + assert!(check_hdl("20.1234/ ").is_err()); + assert!(check_hdl("20.1234.sdf").is_err()); + assert!(check_hdl("20.1234/\naksjdfh").is_err()); + assert!(check_hdl("20.1234").is_err()); + assert!(check_hdl("20.1234/").is_err()); +} + pub fn check_issn(raw: &str) -> Result<()> { lazy_static! { static ref RE: Regex = Regex::new(r"^\d{4}-\d{3}[0-9X]$").unwrap(); @@ -767,3 +807,34 @@ fn test_check_contrib_role() { assert!(check_contrib_role("EDITOR").is_err()); assert!(check_contrib_role("editor ").is_err()); } + +pub fn check_publication_status(raw: &str) -> Result<()> { + let valid_types = vec![ + // Didn't have a controlled vocab so made one up + "active", + "suspended", + "discontinued", + "vanished", + "never", + "one-time", + ]; + for good in valid_types { + if raw == good { + return Ok(()); + } + } + Err(FatcatError::NotInControlledVocabulary( + "publication_status (controlled vocabulary)".to_string(), + raw.to_string(), + ))? +} + +#[test] +fn test_check_publication_status() { + assert!(check_publication_status("active").is_ok()); + assert!(check_publication_status("discontinued").is_ok()); + assert!(check_publication_status("boondogle").is_err()); + assert!(check_publication_status("").is_err()); + assert!(check_publication_status("active ").is_err()); + assert!(check_publication_status("Active").is_err()); +} -- cgit v1.2.3