diff --git a/thoth-app/src/models/work/mod.rs b/thoth-app/src/models/work/mod.rs
index a936d540..d8df5fcf 100644
--- a/thoth-app/src/models/work/mod.rs
+++ b/thoth-app/src/models/work/mod.rs
@@ -119,6 +119,7 @@ pub trait DisplayWork {
fn onix_projectmuse_endpoint(&self) -> String;
fn onix_oapen_endpoint(&self) -> String;
fn onix_jstor_endpoint(&self) -> String;
+ fn onix_ebsco_host_endpoint(&self) -> String;
fn csv_endpoint(&self) -> String;
fn kbart_endpoint(&self) -> String;
fn cover_alt_text(&self) -> String;
@@ -149,6 +150,13 @@ impl DisplayWork for WorkWithRelations {
)
}
+ fn onix_ebsco_host_endpoint(&self) -> String {
+ format!(
+ "{}/specifications/onix_2.1::ebsco_host/work/{}",
+ THOTH_EXPORT_API, &self.work_id
+ )
+ }
+
fn csv_endpoint(&self) -> String {
format!(
"{}/specifications/csv::thoth/work/{}",
@@ -370,6 +378,12 @@ impl DisplayWork for WorkWithRelations {
>
{"ONIX (JSTOR)"}
+
+ {"ONIX (EBSCO Host)"}
+
{}
pub const DELIMITER_COMMA: u8 = b',';
pub const DELIMITER_TAB: u8 = b'\t';
+pub const XML_DECLARATION: &str = "\n";
+pub const DOCTYPE_ONIX21_REF: &str = "\n";
pub(crate) enum MetadataSpecification {
Onix3ProjectMuse(Onix3ProjectMuse),
Onix3Oapen(Onix3Oapen),
Onix3Jstor(Onix3Jstor),
+ Onix21EbscoHost(Onix21EbscoHost),
CsvThoth(CsvThoth),
KbartOclc(KbartOclc),
}
@@ -56,6 +59,7 @@ where
MetadataSpecification::Onix3ProjectMuse(_) => Self::XML_MIME_TYPE,
MetadataSpecification::Onix3Oapen(_) => Self::XML_MIME_TYPE,
MetadataSpecification::Onix3Jstor(_) => Self::XML_MIME_TYPE,
+ MetadataSpecification::Onix21EbscoHost(_) => Self::XML_MIME_TYPE,
MetadataSpecification::CsvThoth(_) => Self::CSV_MIME_TYPE,
MetadataSpecification::KbartOclc(_) => Self::TXT_MIME_TYPE,
}
@@ -66,6 +70,7 @@ where
MetadataSpecification::Onix3ProjectMuse(_) => self.xml_file_name(),
MetadataSpecification::Onix3Oapen(_) => self.xml_file_name(),
MetadataSpecification::Onix3Jstor(_) => self.xml_file_name(),
+ MetadataSpecification::Onix21EbscoHost(_) => self.xml_file_name(),
MetadataSpecification::CsvThoth(_) => self.csv_file_name(),
MetadataSpecification::KbartOclc(_) => self.txt_file_name(),
}
@@ -101,10 +106,17 @@ impl MetadataRecord> {
fn generate(&self) -> ThothResult {
match &self.specification {
MetadataSpecification::Onix3ProjectMuse(onix3_project_muse) => {
- onix3_project_muse.generate(&self.data)
+ onix3_project_muse.generate(&self.data, None)
+ }
+ MetadataSpecification::Onix3Oapen(onix3_oapen) => {
+ onix3_oapen.generate(&self.data, None)
+ }
+ MetadataSpecification::Onix3Jstor(onix3_jstor) => {
+ onix3_jstor.generate(&self.data, None)
+ }
+ MetadataSpecification::Onix21EbscoHost(onix21_ebsco_host) => {
+ onix21_ebsco_host.generate(&self.data, Some(DOCTYPE_ONIX21_REF))
}
- MetadataSpecification::Onix3Oapen(onix3_oapen) => onix3_oapen.generate(&self.data),
- MetadataSpecification::Onix3Jstor(onix3_jstor) => onix3_jstor.generate(&self.data),
MetadataSpecification::CsvThoth(csv_thoth) => {
csv_thoth.generate(&self.data, QuoteStyle::Always, DELIMITER_COMMA)
}
@@ -162,6 +174,9 @@ impl FromStr for MetadataSpecification {
}
"onix_3.0::oapen" => Ok(MetadataSpecification::Onix3Oapen(Onix3Oapen {})),
"onix_3.0::jstor" => Ok(MetadataSpecification::Onix3Jstor(Onix3Jstor {})),
+ "onix_2.1::ebsco_host" => {
+ Ok(MetadataSpecification::Onix21EbscoHost(Onix21EbscoHost {}))
+ }
"csv::thoth" => Ok(MetadataSpecification::CsvThoth(CsvThoth {})),
"kbart::oclc" => Ok(MetadataSpecification::KbartOclc(KbartOclc {})),
_ => Err(ThothError::InvalidMetadataSpecification(input.to_string())),
@@ -175,6 +190,7 @@ impl ToString for MetadataSpecification {
MetadataSpecification::Onix3ProjectMuse(_) => "onix_3.0::project_muse".to_string(),
MetadataSpecification::Onix3Oapen(_) => "onix_3.0::oapen".to_string(),
MetadataSpecification::Onix3Jstor(_) => "onix_3.0::jstor".to_string(),
+ MetadataSpecification::Onix21EbscoHost(_) => "onix_2.1::ebsco_host".to_string(),
MetadataSpecification::CsvThoth(_) => "csv::thoth".to_string(),
MetadataSpecification::KbartOclc(_) => "kbart::oclc".to_string(),
}
@@ -232,6 +248,15 @@ mod tests {
to_test.file_name(),
"onix_3.0__jstor__some_id.xml".to_string()
);
+ let to_test = MetadataRecord::new(
+ "some_id".to_string(),
+ MetadataSpecification::Onix21EbscoHost(Onix21EbscoHost {}),
+ vec![],
+ );
+ assert_eq!(
+ to_test.file_name(),
+ "onix_2.1__ebsco_host__some_id.xml".to_string()
+ );
let to_test = MetadataRecord::new(
"some_id".to_string(),
MetadataSpecification::KbartOclc(KbartOclc {}),
diff --git a/thoth-export-server/src/xml/mod.rs b/thoth-export-server/src/xml/mod.rs
index f181101a..8327da8a 100644
--- a/thoth-export-server/src/xml/mod.rs
+++ b/thoth-export-server/src/xml/mod.rs
@@ -1,3 +1,4 @@
+use crate::record::XML_DECLARATION;
use std::collections::HashMap;
use std::io::Write;
use thoth_client::Work;
@@ -42,9 +43,12 @@ pub(crate) fn write_full_element_block) -> T
}
pub(crate) trait XmlSpecification {
- fn generate(&self, works: &[Work]) -> ThothResult {
- let mut buffer = Vec::new();
+ fn generate(&self, works: &[Work], doctype: Option<&str>) -> ThothResult {
+ let mut buffer = format!("{}{}", XML_DECLARATION, doctype.unwrap_or_default())
+ .as_bytes()
+ .to_vec();
let mut writer = EmitterConfig::new()
+ .write_document_declaration(false)
.perform_indent(true)
.create_writer(&mut buffer);
Self::handle_event(&mut writer, works)
@@ -81,3 +85,5 @@ mod onix3_oapen;
pub(crate) use onix3_oapen::Onix3Oapen;
mod onix3_jstor;
pub(crate) use onix3_jstor::Onix3Jstor;
+mod onix21_ebsco_host;
+pub(crate) use onix21_ebsco_host::Onix21EbscoHost;
diff --git a/thoth-export-server/src/xml/onix21_ebsco_host.rs b/thoth-export-server/src/xml/onix21_ebsco_host.rs
new file mode 100644
index 00000000..1ad42b1a
--- /dev/null
+++ b/thoth-export-server/src/xml/onix21_ebsco_host.rs
@@ -0,0 +1,1101 @@
+use chrono::Utc;
+use std::collections::HashMap;
+use std::io::Write;
+use thoth_client::{
+ ContributionType, CurrencyCode, LanguageRelation, PublicationType, SubjectType, Work,
+ WorkContributions, WorkIssues, WorkLanguages, WorkPublications, WorkStatus, WorkSubjects,
+};
+use xml::writer::{EventWriter, XmlEvent};
+
+use super::{write_element_block, XmlElement, XmlSpecification};
+use crate::xml::{write_full_element_block, XmlElementBlock};
+use thoth_errors::{ThothError, ThothResult};
+
+pub struct Onix21EbscoHost {}
+
+impl XmlSpecification for Onix21EbscoHost {
+ fn handle_event(w: &mut EventWriter, works: &[Work]) -> ThothResult<()> {
+ write_full_element_block("ONIXMessage", None, None, w, |w| {
+ write_element_block("Header", w, |w| {
+ write_element_block("FromCompany", w, |w| {
+ w.write(XmlEvent::Characters("Thoth")).map_err(|e| e.into())
+ })?;
+ write_element_block("FromEmail", w, |w| {
+ w.write(XmlEvent::Characters("info@thoth.pub"))
+ .map_err(|e| e.into())
+ })?;
+ write_element_block("SentDate", w, |w| {
+ w.write(XmlEvent::Characters(
+ &Utc::today().format("%Y%m%d").to_string(),
+ ))
+ .map_err(|e| e.into())
+ })
+ })?;
+
+ match works.len() {
+ 0 => Err(ThothError::IncompleteMetadataRecord(
+ "onix_2.1::ebsco_host".to_string(),
+ "Not enough data".to_string(),
+ )),
+ 1 => XmlElementBlock::::xml_element(works.first().unwrap(), w),
+ _ => {
+ for work in works.iter() {
+ XmlElementBlock::::xml_element(work, w).ok();
+ }
+ Ok(())
+ }
+ }
+ })
+ }
+}
+
+impl XmlElementBlock for Work {
+ fn xml_element(&self, w: &mut EventWriter) -> ThothResult<()> {
+ let work_id = format!("urn:uuid:{}", self.work_id.to_string());
+ let (main_isbn, isbns) = get_publications_data(&self.publications);
+ // We only submit PDFs and EPUBs to EBSCO Host, so don't
+ // generate ONIX for works which do not have either
+ let pdf_url = self
+ .publications
+ .iter()
+ .find(|p| p.publication_type.eq(&PublicationType::PDF))
+ .and_then(|p| p.publication_url.as_ref());
+ let epub_url = self
+ .publications
+ .iter()
+ .find(|p| p.publication_type.eq(&PublicationType::EPUB))
+ .and_then(|p| p.publication_url.as_ref());
+ if pdf_url.is_some() || epub_url.is_some() {
+ write_element_block("Product", w, |w| {
+ write_element_block("RecordReference", w, |w| {
+ w.write(XmlEvent::Characters(&work_id))
+ .map_err(|e| e.into())
+ })?;
+ // 03 Notification confirmed on publication
+ write_element_block("NotificationType", w, |w| {
+ w.write(XmlEvent::Characters("03")).map_err(|e| e.into())
+ })?;
+ // 01 Publisher
+ write_element_block("RecordSourceType", w, |w| {
+ w.write(XmlEvent::Characters("01")).map_err(|e| e.into())
+ })?;
+ write_element_block("ProductIdentifier", w, |w| {
+ // 01 Proprietary
+ write_element_block("ProductIDType", w, |w| {
+ w.write(XmlEvent::Characters("01")).map_err(|e| e.into())
+ })?;
+ write_element_block("IDValue", w, |w| {
+ w.write(XmlEvent::Characters(&work_id))
+ .map_err(|e| e.into())
+ })
+ })?;
+ write_element_block("ProductIdentifier", w, |w| {
+ // 15 ISBN-13
+ write_element_block("ProductIDType", w, |w| {
+ w.write(XmlEvent::Characters("15")).map_err(|e| e.into())
+ })?;
+ write_element_block("IDValue", w, |w| {
+ w.write(XmlEvent::Characters(&main_isbn))
+ .map_err(|e| e.into())
+ })
+ })?;
+ if let Some(doi) = &self.doi {
+ write_element_block("ProductIdentifier", w, |w| {
+ write_element_block("ProductIDType", w, |w| {
+ w.write(XmlEvent::Characters("06")).map_err(|e| e.into())
+ })?;
+ write_element_block("IDValue", w, |w| {
+ w.write(XmlEvent::Characters(&doi.to_string()))
+ .map_err(|e| e.into())
+ })
+ })?;
+ }
+ // DG Electronic book text in proprietary or open standard format
+ write_element_block("ProductForm", w, |w| {
+ w.write(XmlEvent::Characters("DG")).map_err(|e| e.into())
+ })?;
+ write_element_block("EpubType", w, |w| {
+ // 002 PDF
+ let mut epub_type = "002";
+ // We definitely have either a PDF URL or an EPUB URL (or both)
+ if pdf_url.is_none() {
+ // 029 EPUB
+ epub_type = "029";
+ }
+ w.write(XmlEvent::Characters(epub_type))
+ .map_err(|e| e.into())
+ })?;
+ for issue in &self.issues {
+ XmlElementBlock::::xml_element(issue, w).ok();
+ }
+ write_element_block("Title", w, |w| {
+ // 01 Distinctive title (book)
+ write_element_block("TitleType", w, |w| {
+ w.write(XmlEvent::Characters("01")).map_err(|e| e.into())
+ })?;
+ if let Some(subtitle) = &self.subtitle {
+ write_element_block("TitleText", w, |w| {
+ w.write(XmlEvent::Characters(&self.title))
+ .map_err(|e| e.into())
+ })?;
+ write_element_block("Subtitle", w, |w| {
+ w.write(XmlEvent::Characters(subtitle))
+ .map_err(|e| e.into())
+ })
+ } else {
+ write_element_block("TitleText", w, |w| {
+ w.write(XmlEvent::Characters(&self.full_title))
+ .map_err(|e| e.into())
+ })
+ }
+ })?;
+ write_element_block("WorkIdentifier", w, |w| {
+ // 01 Proprietary
+ write_element_block("WorkIDType", w, |w| {
+ w.write(XmlEvent::Characters("01")).map_err(|e| e.into())
+ })?;
+ write_element_block("IDTypeName", w, |w| {
+ w.write(XmlEvent::Characters("Thoth WorkID"))
+ .map_err(|e| e.into())
+ })?;
+ write_element_block("IDValue", w, |w| {
+ w.write(XmlEvent::Characters(&work_id))
+ .map_err(|e| e.into())
+ })
+ })?;
+ let mut websites: HashMap = HashMap::new();
+ if let Some(pdf) = pdf_url {
+ websites.insert(
+ pdf.to_string(),
+ (
+ "29".to_string(),
+ "Publisher's website: download the title".to_string(),
+ ),
+ );
+ }
+ if let Some(epub) = epub_url {
+ websites.insert(
+ epub.to_string(),
+ (
+ "29".to_string(),
+ "Publisher's website: download the title".to_string(),
+ ),
+ );
+ }
+ if let Some(landing_page) = &self.landing_page {
+ websites.insert(
+ landing_page.to_string(),
+ (
+ "01".to_string(),
+ "Publisher's website: web shop".to_string(),
+ ),
+ );
+ }
+ for (url, description) in websites.iter() {
+ write_element_block("Website", w, |w| {
+ // 01 Publisher’s corporate website
+ write_element_block("WebsiteRole", w, |w| {
+ w.write(XmlEvent::Characters(&description.0))
+ .map_err(|e| e.into())
+ })?;
+ write_element_block("WebsiteDescription", w, |w| {
+ w.write(XmlEvent::Characters(&description.1))
+ .map_err(|e| e.into())
+ })?;
+ write_element_block("WebsiteLink", w, |w| {
+ w.write(XmlEvent::Characters(url)).map_err(|e| e.into())
+ })
+ })?;
+ }
+ for contribution in &self.contributions {
+ XmlElementBlock::::xml_element(contribution, w).ok();
+ }
+ for language in &self.languages {
+ XmlElementBlock::::xml_element(language, w).ok();
+ }
+ if let Some(page_count) = self.page_count {
+ write_element_block("Extent", w, |w| {
+ // 00 Main content
+ write_element_block("ExtentType", w, |w| {
+ w.write(XmlEvent::Characters("00")).map_err(|e| e.into())
+ })?;
+ write_element_block("ExtentValue", w, |w| {
+ w.write(XmlEvent::Characters(&page_count.to_string()))
+ .map_err(|e| e.into())
+ })?;
+ // 03 Pages
+ write_element_block("ExtentUnit", w, |w| {
+ w.write(XmlEvent::Characters("03")).map_err(|e| e.into())
+ })
+ })?;
+ }
+ for subject in &self.subjects {
+ XmlElementBlock::::xml_element(subject, w).ok();
+ }
+ write_element_block("Audience", w, |w| {
+ // 01 ONIX audience codes
+ write_element_block("AudienceCodeType", w, |w| {
+ w.write(XmlEvent::Characters("01")).map_err(|e| e.into())
+ })?;
+ // 06 Professional and scholarly
+ write_element_block("AudienceCodeValue", w, |w| {
+ w.write(XmlEvent::Characters("06")).map_err(|e| e.into())
+ })
+ })?;
+ write_element_block("OtherText", w, |w| {
+ // 47 Open access statement
+ // "Should always be accompanied by a link to the complete license (see code 46)"
+ // (not specified as required by EBSCO Host themselves)
+ write_element_block("TextTypeCode", w, |w| {
+ w.write(XmlEvent::Characters("47")).map_err(|e| e.into())
+ })?;
+ write_element_block("Text", w, |w| {
+ w.write(XmlEvent::Characters("Open access - no commercial use"))
+ .map_err(|e| e.into())
+ })
+ })?;
+ if let Some(license) = &self.license {
+ write_element_block("OtherText", w, |w| {
+ // 46 License
+ write_element_block("TextTypeCode", w, |w| {
+ w.write(XmlEvent::Characters("46")).map_err(|e| e.into())
+ })?;
+ write_element_block("Text", w, |w| {
+ w.write(XmlEvent::Characters(license)).map_err(|e| e.into())
+ })
+ })?;
+ }
+ if let Some(labstract) = &self.long_abstract {
+ write_element_block("OtherText", w, |w| {
+ // 03 Long description
+ write_element_block("TextTypeCode", w, |w| {
+ w.write(XmlEvent::Characters("03")).map_err(|e| e.into())
+ })?;
+ // 06 Default text format
+ write_element_block("TextFormat", w, |w| {
+ w.write(XmlEvent::Characters("06")).map_err(|e| e.into())
+ })?;
+ write_element_block("Text", w, |w| {
+ w.write(XmlEvent::Characters(labstract))
+ .map_err(|e| e.into())
+ })
+ })?;
+ }
+ if let Some(cover_url) = &self.cover_url {
+ write_element_block("MediaFile", w, |w| {
+ // 04 Image: front cover
+ write_element_block("MediaFileTypeCode", w, |w| {
+ w.write(XmlEvent::Characters("04")).map_err(|e| e.into())
+ })?;
+ // 01 URL
+ write_element_block("MediaFileLinkTypeCode", w, |w| {
+ w.write(XmlEvent::Characters("01")).map_err(|e| e.into())
+ })?;
+ write_element_block("MediaFileLink", w, |w| {
+ w.write(XmlEvent::Characters(cover_url))
+ .map_err(|e| e.into())
+ })
+ })?;
+ }
+ write_element_block("Imprint", w, |w| {
+ write_element_block("ImprintName", w, |w| {
+ w.write(XmlEvent::Characters(&self.imprint.imprint_name))
+ .map_err(|e| e.into())
+ })
+ })?;
+ write_element_block("Publisher", w, |w| {
+ // 01 Publisher
+ write_element_block("PublishingRole", w, |w| {
+ w.write(XmlEvent::Characters("01")).map_err(|e| e.into())
+ })?;
+ write_element_block("PublisherName", w, |w| {
+ w.write(XmlEvent::Characters(&self.imprint.publisher.publisher_name))
+ .map_err(|e| e.into())
+ })?;
+ if let Some(publisher_url) = &self.imprint.publisher.publisher_url {
+ write_element_block("Website", w, |w| {
+ write_element_block("WebsiteLink", w, |w| {
+ w.write(XmlEvent::Characters(publisher_url))
+ .map_err(|e| e.into())
+ })
+ })?;
+ }
+ Ok(())
+ })?;
+ if let Some(place) = &self.place {
+ write_element_block("CityOfPublication", w, |w| {
+ w.write(XmlEvent::Characters(place)).map_err(|e| e.into())
+ })?;
+ }
+ XmlElement::::xml_element(&self.work_status, w)?;
+ if let Some(date) = self.publication_date {
+ write_element_block("PublicationDate", w, |w| {
+ w.write(XmlEvent::Characters(&date.format("%Y%m%d").to_string()))
+ .map_err(|e| e.into())
+ })?;
+ write_element_block("CopyrightYear", w, |w| {
+ w.write(XmlEvent::Characters(&date.format("%Y").to_string()))
+ .map_err(|e| e.into())
+ })?;
+ }
+ write_element_block("SalesRights", w, |w| {
+ // 02 For sale with non-exclusive rights in the specified countries or territories
+ write_element_block("SalesRightsType", w, |w| {
+ w.write(XmlEvent::Characters("02")).map_err(|e| e.into())
+ })?;
+ write_element_block("RightsTerritory", w, |w| {
+ w.write(XmlEvent::Characters("WORLD")).map_err(|e| e.into())
+ })
+ })?;
+ if !isbns.is_empty() {
+ for isbn in &isbns {
+ write_element_block("RelatedProduct", w, |w| {
+ // 06 Alternative format
+ write_element_block("RelationCode", w, |w| {
+ w.write(XmlEvent::Characters("06")).map_err(|e| e.into())
+ })?;
+ write_element_block("ProductIdentifier", w, |w| {
+ // 15 ISBN-13
+ write_element_block("ProductIDType", w, |w| {
+ w.write(XmlEvent::Characters("15")).map_err(|e| e.into())
+ })?;
+ write_element_block("IDValue", w, |w| {
+ w.write(XmlEvent::Characters(isbn)).map_err(|e| e.into())
+ })
+ })
+ })?;
+ }
+ }
+ write_element_block("SupplyDetail", w, |w| {
+ write_element_block("SupplierName", w, |w| {
+ w.write(XmlEvent::Characters(&self.imprint.publisher.publisher_name))
+ .map_err(|e| e.into())
+ })?;
+ // 09 Publisher to end-customers
+ write_element_block("SupplierRole", w, |w| {
+ w.write(XmlEvent::Characters("09")).map_err(|e| e.into())
+ })?;
+ // 99 Contact supplier
+ write_element_block("ProductAvailability", w, |w| {
+ w.write(XmlEvent::Characters("99")).map_err(|e| e.into())
+ })?;
+ // R Restrictions apply, see note
+ write_element_block("AudienceRestrictionFlag", w, |w| {
+ w.write(XmlEvent::Characters("R")).map_err(|e| e.into())
+ })?;
+ write_element_block("AudienceRestrictionNote", w, |w| {
+ w.write(XmlEvent::Characters("Open access"))
+ .map_err(|e| e.into())
+ })?;
+ // Works are distributed to EBSCO Host as combined PDF/EPUB
+ // "digital bundles" - PDF and EPUB may have different prices
+ // so give the higher of the two. If both are free, EBSCO Host
+ // request a price point of "0.01 USD" for Open Access titles.
+ let pdf_price = self
+ .publications
+ .iter()
+ .find(|p| p.publication_type.eq(&PublicationType::PDF))
+ .and_then(|p| {
+ p.prices
+ .iter()
+ .find(|pr| pr.currency_code.eq(&CurrencyCode::USD))
+ .map(|pr| pr.unit_price)
+ })
+ .unwrap_or_default();
+ let epub_price = self
+ .publications
+ .iter()
+ .find(|p| p.publication_type.eq(&PublicationType::EPUB))
+ .and_then(|p| {
+ p.prices
+ .iter()
+ .find(|pr| pr.currency_code.eq(&CurrencyCode::USD))
+ .map(|pr| pr.unit_price)
+ })
+ .unwrap_or_default();
+ let bundle_price = pdf_price.max(epub_price.max(0.01));
+ write_element_block("Price", w, |w| {
+ // 02 RRP including tax
+ write_element_block("PriceTypeCode", w, |w| {
+ w.write(XmlEvent::Characters("02")).map_err(|e| e.into())
+ })?;
+ write_element_block("PriceAmount", w, |w| {
+ w.write(XmlEvent::Characters(&bundle_price.to_string()))
+ .map_err(|e| e.into())
+ })?;
+ write_element_block("CurrencyCode", w, |w| {
+ w.write(XmlEvent::Characters("USD")).map_err(|e| e.into())
+ })
+ })
+ })
+ })
+ } else {
+ Err(ThothError::IncompleteMetadataRecord(
+ "onix_2.1::ebsco_host".to_string(),
+ "No PDF or EPUB URL".to_string(),
+ ))
+ }
+ }
+}
+
+fn get_publications_data(publications: &[WorkPublications]) -> (String, Vec) {
+ let mut main_isbn = "".to_string();
+ let mut isbns: Vec = Vec::new();
+
+ for publication in publications {
+ if let Some(isbn) = &publication.isbn.as_ref().map(|i| i.to_string()) {
+ isbns.push(isbn.replace("-", ""));
+ // The default product ISBN is the PDF's
+ if publication.publication_type.eq(&PublicationType::PDF) {
+ main_isbn = isbn.replace("-", "");
+ }
+ // Books that don't have a PDF ISBN will use the paperback's
+ if publication.publication_type.eq(&PublicationType::PAPERBACK) && main_isbn.is_empty()
+ {
+ main_isbn = isbn.replace("-", "");
+ }
+ }
+ }
+
+ (main_isbn, isbns)
+}
+
+impl XmlElement for WorkStatus {
+ const ELEMENT: &'static str = "PublishingStatus";
+
+ fn value(&self) -> &'static str {
+ match self {
+ WorkStatus::UNSPECIFIED => "00",
+ WorkStatus::CANCELLED => "01",
+ WorkStatus::FORTHCOMING => "02",
+ WorkStatus::POSTPONED_INDEFINITELY => "03",
+ WorkStatus::ACTIVE => "04",
+ WorkStatus::NO_LONGER_OUR_PRODUCT => "05",
+ WorkStatus::OUT_OF_STOCK_INDEFINITELY => "06",
+ WorkStatus::OUT_OF_PRINT => "07",
+ WorkStatus::INACTIVE => "08",
+ WorkStatus::UNKNOWN => "09",
+ WorkStatus::REMAINDERED => "10",
+ WorkStatus::WITHDRAWN_FROM_SALE => "11",
+ WorkStatus::RECALLED => "15",
+ WorkStatus::Other(_) => unreachable!(),
+ }
+ }
+}
+
+impl XmlElement for SubjectType {
+ const ELEMENT: &'static str = "SubjectSchemeIdentifier";
+
+ fn value(&self) -> &'static str {
+ match self {
+ SubjectType::BIC => "12",
+ SubjectType::BISAC => "10",
+ SubjectType::KEYWORD => "20",
+ SubjectType::LCC => "04",
+ SubjectType::THEMA => "93",
+ SubjectType::CUSTOM => "B2",
+ SubjectType::Other(_) => unreachable!(),
+ }
+ }
+}
+
+impl XmlElement for LanguageRelation {
+ const ELEMENT: &'static str = "LanguageRole";
+
+ fn value(&self) -> &'static str {
+ match self {
+ LanguageRelation::ORIGINAL => "01",
+ LanguageRelation::TRANSLATED_FROM => "02",
+ LanguageRelation::TRANSLATED_INTO => "01",
+ LanguageRelation::Other(_) => unreachable!(),
+ }
+ }
+}
+
+impl XmlElement for ContributionType {
+ const ELEMENT: &'static str = "ContributorRole";
+
+ fn value(&self) -> &'static str {
+ match self {
+ ContributionType::AUTHOR => "A01",
+ ContributionType::EDITOR => "B01",
+ ContributionType::TRANSLATOR => "B06",
+ ContributionType::PHOTOGRAPHER => "A13",
+ ContributionType::ILUSTRATOR => "A12",
+ ContributionType::MUSIC_EDITOR => "B25",
+ ContributionType::FOREWORD_BY => "A23",
+ ContributionType::INTRODUCTION_BY => "A24",
+ ContributionType::AFTERWORD_BY => "A19",
+ ContributionType::PREFACE_BY => "A15",
+ ContributionType::Other(_) => unreachable!(),
+ }
+ }
+}
+
+impl XmlElementBlock for WorkContributions {
+ fn xml_element(&self, w: &mut EventWriter) -> ThothResult<()> {
+ write_element_block("Contributor", w, |w| {
+ write_element_block("SequenceNumber", w, |w| {
+ w.write(XmlEvent::Characters(&self.contribution_ordinal.to_string()))
+ .map_err(|e| e.into())
+ })?;
+ XmlElement::::xml_element(&self.contribution_type, w)?;
+
+ if let Some(orcid) = &self.contributor.orcid {
+ write_element_block("PersonNameIdentifier", w, |w| {
+ // 01 Proprietary
+ write_element_block("PersonNameIDType", w, |w| {
+ w.write(XmlEvent::Characters("01")).map_err(|e| e.into())
+ })?;
+ write_element_block("IDTypeName", w, |w| {
+ w.write(XmlEvent::Characters("ORCID")).map_err(|e| e.into())
+ })?;
+ write_element_block("IDValue", w, |w| {
+ w.write(XmlEvent::Characters(&orcid.to_string()))
+ .map_err(|e| e.into())
+ })
+ })?;
+ }
+ if let Some(first_name) = &self.first_name {
+ write_element_block("NamesBeforeKey", w, |w| {
+ w.write(XmlEvent::Characters(first_name))
+ .map_err(|e| e.into())
+ })?;
+ write_element_block("KeyNames", w, |w| {
+ w.write(XmlEvent::Characters(&self.last_name))
+ .map_err(|e| e.into())
+ })?;
+ } else {
+ write_element_block("PersonName", w, |w| {
+ w.write(XmlEvent::Characters(&self.full_name))
+ .map_err(|e| e.into())
+ })?;
+ }
+ Ok(())
+ })
+ }
+}
+
+impl XmlElementBlock for WorkLanguages {
+ fn xml_element(&self, w: &mut EventWriter) -> ThothResult<()> {
+ write_element_block("Language", w, |w| {
+ XmlElement::::xml_element(&self.language_relation, w).ok();
+ // not worth implementing XmlElement for LanguageCode as all cases would
+ // need to be exhaustively matched and the codes are equivalent anyway
+ write_element_block("LanguageCode", w, |w| {
+ w.write(XmlEvent::Characters(
+ &self.language_code.to_string().to_lowercase(),
+ ))
+ .map_err(|e| e.into())
+ })
+ })
+ }
+}
+
+impl XmlElementBlock for WorkIssues {
+ fn xml_element(&self, w: &mut EventWriter) -> ThothResult<()> {
+ write_element_block("Series", w, |w| {
+ write_element_block("TitleOfSeries", w, |w| {
+ w.write(XmlEvent::Characters(&self.series.series_name))
+ .map_err(|e| e.into())
+ })?;
+ write_element_block("NumberWithinSeries", w, |w| {
+ w.write(XmlEvent::Characters(&self.issue_ordinal.to_string()))
+ .map_err(|e| e.into())
+ })
+ })
+ }
+}
+
+impl XmlElementBlock for WorkSubjects {
+ fn xml_element(&self, w: &mut EventWriter) -> ThothResult<()> {
+ write_element_block("Subject", w, |w| {
+ XmlElement::::xml_element(&self.subject_type, w)?;
+ match self.subject_type {
+ SubjectType::KEYWORD | SubjectType::CUSTOM => {
+ write_element_block("SubjectHeadingText", w, |w| {
+ w.write(XmlEvent::Characters(&self.subject_code))
+ .map_err(|e| e.into())
+ })
+ }
+ _ => write_element_block("SubjectCode", w, |w| {
+ w.write(XmlEvent::Characters(&self.subject_code))
+ .map_err(|e| e.into())
+ }),
+ }
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ // Testing note: XML nodes cannot be guaranteed to be output in the same order every time
+ // We therefore rely on `assert!(contains)` rather than `assert_eq!`
+ use super::*;
+ use std::str::FromStr;
+ use thoth_api::model::Doi;
+ use thoth_api::model::Isbn;
+ use thoth_api::model::Orcid;
+ use thoth_client::WorkPublicationsPrices;
+ use thoth_client::{
+ ContributionType, LanguageCode, LanguageRelation, PublicationType,
+ WorkContributionsContributor, WorkImprint, WorkImprintPublisher, WorkIssuesSeries,
+ WorkStatus, WorkType,
+ };
+ use uuid::Uuid;
+
+ fn generate_test_output(input: &impl XmlElementBlock) -> String {
+ // Helper function based on `XmlSpecification::generate`
+ let mut buffer = Vec::new();
+ let mut writer = xml::writer::EmitterConfig::new()
+ .perform_indent(true)
+ .create_writer(&mut buffer);
+ let wrapped_output = XmlElementBlock::::xml_element(input, &mut writer)
+ .map(|_| buffer)
+ .and_then(|onix| {
+ String::from_utf8(onix)
+ .map_err(|_| ThothError::InternalError("Could not parse XML".to_string()))
+ });
+ assert!(wrapped_output.is_ok());
+ wrapped_output.unwrap()
+ }
+
+ #[test]
+ fn test_onix21_ebsco_host_contributions() {
+ let mut test_contribution = WorkContributions {
+ contribution_type: ContributionType::AUTHOR,
+ first_name: Some("Author".to_string()),
+ last_name: "1".to_string(),
+ full_name: "Author 1".to_string(),
+ main_contribution: true,
+ biography: None,
+ institution: None,
+ contribution_ordinal: 1,
+ contributor: WorkContributionsContributor {
+ orcid: Some(Orcid::from_str("https://orcid.org/0000-0002-0000-0001").unwrap()),
+ },
+ };
+
+ // Test standard output
+ let output = generate_test_output(&test_contribution);
+ assert!(output.contains(r#" 1"#));
+ assert!(output.contains(r#" A01"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 01"#));
+ assert!(output.contains(r#" ORCID"#));
+ assert!(output.contains(r#" 0000-0002-0000-0001"#));
+ assert!(output.contains(r#" "#));
+ // Given name is output as NamesBeforeKey and family name as KeyNames
+ assert!(output.contains(r#" Author"#));
+ assert!(output.contains(r#" 1"#));
+ // PersonName is not output when given name is supplied
+ assert!(!output.contains(r#" Author 1"#));
+
+ // Change all possible values to test that output is updated
+ test_contribution.contribution_type = ContributionType::EDITOR;
+ test_contribution.contribution_ordinal = 2;
+ test_contribution.contributor.orcid = None;
+ test_contribution.first_name = None;
+ let output = generate_test_output(&test_contribution);
+ assert!(output.contains(r#" 2"#));
+ assert!(output.contains(r#" B01"#));
+ // No ORCID supplied
+ assert!(!output.contains(r#" "#));
+ assert!(!output.contains(r#" 01"#));
+ assert!(!output.contains(r#" ORCID"#));
+ assert!(!output.contains(r#" 0000-0002-0000-0001"#));
+ assert!(!output.contains(r#" "#));
+ // No given name supplied, so PersonName is output instead of KeyNames and NamesBeforeKey
+ assert!(!output.contains(r#" Author"#));
+ assert!(!output.contains(r#" 1"#));
+ assert!(output.contains(r#" Author 1"#));
+
+ // Test all remaining contributor roles
+ test_contribution.contribution_type = ContributionType::TRANSLATOR;
+ let output = generate_test_output(&test_contribution);
+ assert!(output.contains(r#" B06"#));
+ test_contribution.contribution_type = ContributionType::PHOTOGRAPHER;
+ let output = generate_test_output(&test_contribution);
+ assert!(output.contains(r#" A13"#));
+ test_contribution.contribution_type = ContributionType::ILUSTRATOR;
+ let output = generate_test_output(&test_contribution);
+ assert!(output.contains(r#" A12"#));
+ test_contribution.contribution_type = ContributionType::MUSIC_EDITOR;
+ let output = generate_test_output(&test_contribution);
+ assert!(output.contains(r#" B25"#));
+ test_contribution.contribution_type = ContributionType::FOREWORD_BY;
+ let output = generate_test_output(&test_contribution);
+ assert!(output.contains(r#" A23"#));
+ test_contribution.contribution_type = ContributionType::INTRODUCTION_BY;
+ let output = generate_test_output(&test_contribution);
+ assert!(output.contains(r#" A24"#));
+ test_contribution.contribution_type = ContributionType::AFTERWORD_BY;
+ let output = generate_test_output(&test_contribution);
+ assert!(output.contains(r#" A19"#));
+ test_contribution.contribution_type = ContributionType::PREFACE_BY;
+ let output = generate_test_output(&test_contribution);
+ assert!(output.contains(r#" A15"#));
+ }
+
+ #[test]
+ fn test_onix21_ebsco_host_languages() {
+ let mut test_language = WorkLanguages {
+ language_code: LanguageCode::SPA,
+ language_relation: LanguageRelation::TRANSLATED_FROM,
+ main_language: true,
+ };
+
+ // Test standard output
+ let output = generate_test_output(&test_language);
+ assert!(output.contains(r#" 02"#));
+ assert!(output.contains(r#" spa"#));
+
+ // Change all possible values to test that output is updated
+ test_language.language_code = LanguageCode::WEL;
+ for language_relation in [
+ LanguageRelation::ORIGINAL,
+ LanguageRelation::TRANSLATED_INTO,
+ ] {
+ test_language.language_relation = language_relation;
+ let output = generate_test_output(&test_language);
+ assert!(output.contains(r#" 01"#));
+ assert!(output.contains(r#" wel"#));
+ }
+ }
+
+ #[test]
+ fn test_onix21_ebsco_host_issues() {
+ let mut test_issue = WorkIssues {
+ issue_ordinal: 1,
+ series: WorkIssuesSeries {
+ series_type: thoth_client::SeriesType::JOURNAL,
+ series_name: "Name of series".to_string(),
+ issn_print: "1234-5678".to_string(),
+ issn_digital: "8765-4321".to_string(),
+ series_url: None,
+ },
+ };
+
+ // Test standard output
+ let output = generate_test_output(&test_issue);
+ assert!(output.contains(r#""#));
+ assert!(output.contains(r#" Name of series"#));
+ assert!(output.contains(r#" 1"#));
+
+ // Change all possible values to test that output is updated
+ test_issue.issue_ordinal = 2;
+ test_issue.series.series_name = "Different series".to_string();
+ let output = generate_test_output(&test_issue);
+ assert!(output.contains(r#""#));
+ assert!(output.contains(r#" Different series"#));
+ assert!(output.contains(r#" 2"#));
+ }
+
+ #[test]
+ fn test_onix21_ebsco_host_subjects() {
+ let mut test_subject = WorkSubjects {
+ subject_code: "AAB".to_string(),
+ subject_type: SubjectType::BIC,
+ subject_ordinal: 1,
+ };
+
+ // Test BIC output
+ let output = generate_test_output(&test_subject);
+ assert!(output.contains(r#""#));
+ assert!(output.contains(r#" 12"#));
+ assert!(output.contains(r#" AAB"#));
+
+ // Test BISAC output
+ test_subject.subject_code = "AAA000000".to_string();
+ test_subject.subject_type = SubjectType::BISAC;
+ let output = generate_test_output(&test_subject);
+ assert!(output.contains(r#""#));
+ assert!(output.contains(r#" 10"#));
+ assert!(output.contains(r#" AAA000000"#));
+
+ // Test LCC output
+ test_subject.subject_code = "JA85".to_string();
+ test_subject.subject_type = SubjectType::LCC;
+ let output = generate_test_output(&test_subject);
+ assert!(output.contains(r#""#));
+ assert!(output.contains(r#" 04"#));
+ assert!(output.contains(r#" JA85"#));
+
+ // Test Thema output
+ test_subject.subject_code = "JWA".to_string();
+ test_subject.subject_type = SubjectType::THEMA;
+ let output = generate_test_output(&test_subject);
+ assert!(output.contains(r#""#));
+ assert!(output.contains(r#" 93"#));
+ assert!(output.contains(r#" JWA"#));
+
+ // Test keyword output
+ test_subject.subject_code = "keyword1".to_string();
+ test_subject.subject_type = SubjectType::KEYWORD;
+ let output = generate_test_output(&test_subject);
+ assert!(output.contains(r#""#));
+ assert!(output.contains(r#" 20"#));
+ assert!(output.contains(r#" keyword1"#));
+
+ // Test custom output
+ test_subject.subject_code = "custom1".to_string();
+ test_subject.subject_type = SubjectType::CUSTOM;
+ let output = generate_test_output(&test_subject);
+ assert!(output.contains(r#" B2"#));
+ assert!(output.contains(r#" custom1"#));
+ }
+
+ #[test]
+ fn test_onix21_ebsco_host_works() {
+ let mut test_work = Work {
+ work_id: Uuid::from_str("00000000-0000-0000-AAAA-000000000001").unwrap(),
+ work_status: WorkStatus::ACTIVE,
+ full_title: "Book Title: Book Subtitle".to_string(),
+ title: "Book Title".to_string(),
+ subtitle: Some("Separate Subtitle".to_string()),
+ work_type: WorkType::MONOGRAPH,
+ edition: 1,
+ doi: Some(Doi::from_str("https://doi.org/10.00001/BOOK.0001").unwrap()),
+ publication_date: Some(chrono::NaiveDate::from_ymd(1999, 12, 31)),
+ license: Some("https://creativecommons.org/licenses/by/4.0/".to_string()),
+ copyright_holder: "Author 1; Author 2".to_string(),
+ short_abstract: None,
+ long_abstract: Some("Lorem ipsum dolor sit amet".to_string()),
+ general_note: None,
+ place: Some("León, Spain".to_string()),
+ width_mm: None,
+ width_cm: None,
+ width_in: None,
+ height_mm: None,
+ height_cm: None,
+ height_in: None,
+ page_count: Some(334),
+ page_breakdown: None,
+ image_count: None,
+ table_count: None,
+ audio_count: None,
+ video_count: None,
+ landing_page: Some("https://www.book.com".to_string()),
+ toc: None,
+ lccn: None,
+ oclc: None,
+ cover_url: Some("https://www.book.com/cover".to_string()),
+ cover_caption: None,
+ imprint: WorkImprint {
+ imprint_name: "OA Editions Imprint".to_string(),
+ publisher: WorkImprintPublisher {
+ publisher_name: "OA Editions".to_string(),
+ publisher_url: Some("https://www.publisher.com".to_string()),
+ },
+ },
+ issues: vec![],
+ contributions: vec![],
+ languages: vec![],
+ publications: vec![
+ WorkPublications {
+ publication_id: Uuid::from_str("00000000-0000-0000-AAAA-000000000001").unwrap(),
+ publication_type: PublicationType::EPUB,
+ publication_url: Some("https://www.book.com/epub".to_string()),
+ isbn: Some(Isbn::from_str("978-3-16-148410-0").unwrap()),
+ prices: vec![],
+ },
+ WorkPublications {
+ publication_id: Uuid::from_str("00000000-0000-0000-DDDD-000000000004").unwrap(),
+ publication_type: PublicationType::PDF,
+ publication_url: Some("https://www.book.com/pdf".to_string()),
+ isbn: Some(Isbn::from_str("978-1-56619-909-4").unwrap()),
+ prices: vec![
+ WorkPublicationsPrices {
+ currency_code: CurrencyCode::EUR,
+ unit_price: 5.95,
+ },
+ WorkPublicationsPrices {
+ currency_code: CurrencyCode::GBP,
+ unit_price: 4.95,
+ },
+ WorkPublicationsPrices {
+ currency_code: CurrencyCode::USD,
+ unit_price: 7.99,
+ },
+ ],
+ },
+ ],
+ subjects: vec![],
+ fundings: vec![],
+ };
+
+ // Test standard output
+ let output = generate_test_output(&test_work);
+ assert!(output.contains(r#""#));
+ assert!(output.contains(
+ r#" urn:uuid:00000000-0000-0000-aaaa-000000000001"#
+ ));
+ assert!(output.contains(r#" 03"#));
+ assert!(output.contains(r#" 01"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 01"#));
+ assert!(output
+ .contains(r#" urn:uuid:00000000-0000-0000-aaaa-000000000001"#));
+ assert!(output.contains(r#" 15"#));
+ assert!(output.contains(r#" 9783161484100"#));
+ assert!(output.contains(r#" 06"#));
+ assert!(output.contains(r#" 10.00001/BOOK.0001"#));
+ assert!(output.contains(r#" DG"#));
+ assert!(output.contains(r#" 002"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 01"#));
+ assert!(output.contains(r#" Book Title"#));
+ assert!(output.contains(r#" Separate Subtitle"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 01"#));
+ assert!(output.contains(r#" Thoth WorkID"#));
+ assert!(output
+ .contains(r#" urn:uuid:00000000-0000-0000-aaaa-000000000001"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 01"#));
+ assert!(output.contains(
+ r#" Publisher's website: web shop"#
+ ));
+ assert!(output.contains(r#" https://www.book.com"#));
+ assert!(output.contains(r#" 29"#));
+ assert!(output.contains(r#" Publisher's website: download the title"#));
+ assert!(output.contains(r#" https://www.book.com/epub"#));
+ assert!(output.contains(r#" https://www.book.com/pdf"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 00"#));
+ assert!(output.contains(r#" 334"#));
+ assert!(output.contains(r#" 03"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 01"#));
+ assert!(output.contains(r#" 06"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 47"#));
+ assert!(output.contains(r#" Open access - no commercial use"#));
+ assert!(output.contains(r#" 46"#));
+ assert!(output.contains(r#" https://creativecommons.org/licenses/by/4.0/"#));
+ assert!(output.contains(r#" 03"#));
+ assert!(output.contains(r#" 06"#));
+ assert!(output.contains(r#" Lorem ipsum dolor sit amet"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 04"#));
+ assert!(output.contains(r#" 01"#));
+ assert!(output.contains(r#" https://www.book.com/cover"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" OA Editions Imprint"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 01"#));
+ assert!(output.contains(r#" OA Editions"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" https://www.publisher.com"#));
+ assert!(output.contains(r#" León, Spain"#));
+ assert!(output.contains(r#" 04"#));
+ assert!(output.contains(r#" 19991231"#));
+ assert!(output.contains(r#" 1999"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 02"#));
+ assert!(output.contains(r#" WORLD"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 06"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 15"#));
+ assert!(output.contains(r#" 9783161484100"#));
+ assert!(output.contains(r#" 9781566199094"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" OA Editions"#));
+ assert!(output.contains(r#" 09"#));
+ assert!(output.contains(r#" 99"#));
+ assert!(output.contains(r#" R"#));
+ assert!(output
+ .contains(r#" Open access"#));
+ assert!(output.contains(r#" "#));
+ assert!(output.contains(r#" 02"#));
+ assert!(output.contains(r#" 7.99"#));
+ assert!(output.contains(r#" USD"#));
+
+ // Remove some values to test non-output of optional blocks
+ test_work.doi = None;
+ test_work.license = None;
+ test_work.subtitle = None;
+ test_work.page_count = None;
+ test_work.long_abstract = None;
+ test_work.place = None;
+ test_work.publication_date = None;
+ test_work.license = None;
+ test_work.landing_page = None;
+ test_work.cover_url = None;
+ test_work.imprint.publisher.publisher_url = None;
+ test_work.publications.pop(); // Remove second (PDF) publication
+ let output = generate_test_output(&test_work);
+ // No DOI supplied
+ assert!(!output.contains(r#" 06"#));
+ assert!(!output.contains(r#" 10.00001/BOOK.0001"#));
+ // No subtitle supplied: work FullTitle is used instead of Title
+ assert!(!output.contains(r#" Book Title"#));
+ assert!(!output.contains(r#" Separate Subtitle"#));
+ assert!(output.contains(r#" Book Title: Book Subtitle"#));
+ // No landing page supplied
+ assert!(!output.contains(r#" 01"#));
+ assert!(!output.contains(
+ r#" Publisher's website: web shop"#
+ ));
+ assert!(!output.contains(r#" https://www.book.com"#));
+ // PDF publication removed, hence no PDF URL,
+ // no PDF RelatedProduct, and EpubType changes
+ assert!(!output.contains(r#" https://www.book.com/pdf"#));
+ assert!(!output.contains(r#" 9781566199094"#));
+ assert!(!output.contains(r#" 002"#));
+ assert!(output.contains(r#" 029"#));
+ // No page count supplied
+ assert!(!output.contains(r#" "#));
+ assert!(!output.contains(r#" 00"#));
+ assert!(!output.contains(r#" 334"#));
+ assert!(!output.contains(r#" 03"#));
+ // No long abstract supplied
+ assert!(!output.contains(r#" 03"#));
+ assert!(!output.contains(r#" 06"#));
+ assert!(!output.contains(r#" Lorem ipsum dolor sit amet"#));
+ // No licence supplied
+ assert!(!output.contains(r#" 46"#));
+ assert!(
+ !output.contains(r#" https://creativecommons.org/licenses/by/4.0/"#)
+ );
+ // No cover URL supplied
+ assert!(!output.contains(r#" "#));
+ assert!(!output.contains(r#" 04"#));
+ assert!(!output.contains(r#" 01"#));
+ assert!(
+ !output.contains(r#" https://www.book.com/cover"#)
+ );
+ // No publisher website supplied (and no other blocks remain)
+ assert!(!output.contains(r#" "#));
+ assert!(!output.contains(r#" https://www.publisher.com"#));
+ // No place supplied
+ assert!(!output.contains(r#" León, Spain"#));
+ // No publication date supplied
+ assert!(!output.contains(r#" 19991231"#));
+ assert!(!output.contains(r#" 1999"#));
+ // No PDF or EPUB price supplied, so default of 0.01 USD is used
+ assert!(output.contains(r#" 0.01"#));
+
+ // Remove the remaining (EPUB) publication's URL: error
+ test_work.publications[0].publication_url = None;
+ // Can't use helper function for this as it assumes Ok rather than Err
+ let mut buffer = Vec::new();
+ let mut writer = xml::writer::EmitterConfig::new()
+ .perform_indent(true)
+ .create_writer(&mut buffer);
+ let wrapped_output =
+ XmlElementBlock::::xml_element(&test_work, &mut writer)
+ .map(|_| buffer)
+ .and_then(|onix| {
+ String::from_utf8(onix)
+ .map_err(|_| ThothError::InternalError("Could not parse XML".to_string()))
+ });
+ assert!(wrapped_output.is_err());
+ let output = wrapped_output.unwrap_err().to_string();
+ assert_eq!(
+ output,
+ "Could not generate onix_2.1::ebsco_host: No PDF or EPUB URL".to_string()
+ );
+ }
+}
diff --git a/thoth-export-server/src/xml/onix3_jstor.rs b/thoth-export-server/src/xml/onix3_jstor.rs
index c4bf3f08..8d47b628 100644
--- a/thoth-export-server/src/xml/onix3_jstor.rs
+++ b/thoth-export-server/src/xml/onix3_jstor.rs
@@ -707,6 +707,7 @@ mod tests {
imprint_name: "OA Editions Imprint".to_string(),
publisher: WorkImprintPublisher {
publisher_name: "OA Editions".to_string(),
+ publisher_url: None,
},
},
issues: vec![WorkIssues {
diff --git a/thoth-export-server/src/xml/onix3_oapen.rs b/thoth-export-server/src/xml/onix3_oapen.rs
index 05d3c538..2144ebbd 100644
--- a/thoth-export-server/src/xml/onix3_oapen.rs
+++ b/thoth-export-server/src/xml/onix3_oapen.rs
@@ -980,6 +980,7 @@ mod tests {
imprint_name: "OA Editions Imprint".to_string(),
publisher: WorkImprintPublisher {
publisher_name: "OA Editions".to_string(),
+ publisher_url: None,
},
},
issues: vec![],
diff --git a/thoth-export-server/src/xml/onix3_project_muse.rs b/thoth-export-server/src/xml/onix3_project_muse.rs
index b7b53fcf..c0a1ee74 100644
--- a/thoth-export-server/src/xml/onix3_project_muse.rs
+++ b/thoth-export-server/src/xml/onix3_project_muse.rs
@@ -696,6 +696,7 @@ mod tests {
imprint_name: "OA Editions Imprint".to_string(),
publisher: WorkImprintPublisher {
publisher_name: "OA Editions".to_string(),
+ publisher_url: None,
},
},
issues: vec![WorkIssues {