From 08d29b196d6b760588ffd29f011321cc61ae3512 Mon Sep 17 00:00:00 2001 From: Javier Arias Date: Mon, 12 Jul 2021 16:28:02 +0200 Subject: [PATCH 01/14] Get docker image version from release tag --- .../docker_build_and_push_to_dockerhub.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker_build_and_push_to_dockerhub.yml b/.github/workflows/docker_build_and_push_to_dockerhub.yml index 7884a0e8..6d4a03f9 100644 --- a/.github/workflows/docker_build_and_push_to_dockerhub.yml +++ b/.github/workflows/docker_build_and_push_to_dockerhub.yml @@ -8,6 +8,20 @@ jobs: build_and_push_docker_image: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + # list of Docker images to use as base name for tags + images: | + openbookpublishers/thoth + # generate Docker tags based on the following events/attributes + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx @@ -22,7 +36,8 @@ jobs: uses: docker/build-push-action@v2 with: push: true - tags: openbookpublishers/thoth:latest + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} build-args: | THOTH_GRAPHQL_API=https://api.thoth.pub THOTH_EXPORT_API=https://export.thoth.pub From b016120bc7526d78f8ce0641d4b6aaf30e2537e5 Mon Sep 17 00:00:00 2001 From: Javier Arias Date: Mon, 12 Jul 2021 16:37:41 +0200 Subject: [PATCH 02/14] Add workflow to create releases based on tags --- .github/workflows/create_release.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/create_release.yml diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml new file mode 100644 index 00000000..425574ae --- /dev/null +++ b/.github/workflows/create_release.yml @@ -0,0 +1,27 @@ +name: Create Release + +on: + push: + tags: + - "*.*.*" + +jobs: + build: + name: Create release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Extract release notes + id: extract-release-notes + uses: ffurrer2/extract-release-notes@v1 + - name: Create release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: false + prerelease: false + body: ${{ steps.extract-release-notes.outputs.release_notes }} From bff24a81a9daf02cfbf49a7a64345ff7fb7c7a61 Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Fri, 16 Jul 2021 09:09:18 +0100 Subject: [PATCH 03/14] Add infrastructure for OAPEN output (currently identical to Project Muse output) --- thoth-export-server/src/data.rs | 25 +- thoth-export-server/src/record.rs | 19 +- thoth-export-server/src/xml/mod.rs | 2 + thoth-export-server/src/xml/onix3_oapen.rs | 522 +++++++++++++++++++++ 4 files changed, 562 insertions(+), 6 deletions(-) create mode 100644 thoth-export-server/src/xml/onix3_oapen.rs diff --git a/thoth-export-server/src/data.rs b/thoth-export-server/src/data.rs index 5dff815d..e8e591d7 100644 --- a/thoth-export-server/src/data.rs +++ b/thoth-export-server/src/data.rs @@ -13,6 +13,12 @@ lazy_static! { format: concat!(env!("THOTH_EXPORT_API"), "/formats/onix_3.0"), accepted_by: vec![concat!(env!("THOTH_EXPORT_API"), "/platforms/project_muse"),], }, + Specification { + id: "onix_3.0::oapen", + name: "OAPEN ONIX 3.0", + format: concat!(env!("THOTH_EXPORT_API"), "/formats/onix_3.0"), + accepted_by: vec![concat!(env!("THOTH_EXPORT_API"), "/platforms/oapen"),], + }, Specification { id: "csv::thoth", name: "Thoth CSV", @@ -37,16 +43,27 @@ lazy_static! { "/specifications/onix_3.0::project_muse" ),], }, + Platform { + id: "oapen", + name: "OAPEN", + accepts: vec![concat!( + env!("THOTH_EXPORT_API"), + "/specifications/onix_3.0::oapen" + ),], + }, ]; pub(crate) static ref ALL_FORMATS: Vec> = vec![ Format { id: "onix_3.0", name: "ONIX", version: Some("3.0"), - specifications: vec![concat!( - env!("THOTH_EXPORT_API"), - "/specifications/onix_3.0::project_muse" - ),], + specifications: vec![ + concat!( + env!("THOTH_EXPORT_API"), + "/specifications/onix_3.0::project_muse" + ), + concat!(env!("THOTH_EXPORT_API"), "/specifications/onix_3.0::oapen"), + ], }, Format { id: "csv", diff --git a/thoth-export-server/src/record.rs b/thoth-export-server/src/record.rs index fbb6ccd9..917a80a8 100644 --- a/thoth-export-server/src/record.rs +++ b/thoth-export-server/src/record.rs @@ -9,13 +9,14 @@ use thoth_client::Work; use thoth_errors::{ThothError, ThothResult}; use crate::csv::{CsvSpecification, CsvThoth}; -use crate::xml::{Onix3ProjectMuse, XmlSpecification}; +use crate::xml::{Onix3Oapen, Onix3ProjectMuse, XmlSpecification}; pub(crate) trait AsRecord {} impl AsRecord for Vec {} pub(crate) enum MetadataSpecification { Onix3ProjectMuse(Onix3ProjectMuse), + Onix3Oapen(Onix3Oapen), CsvThoth(CsvThoth), } @@ -45,6 +46,7 @@ where fn content_type(&self) -> &'static str { match &self.specification { MetadataSpecification::Onix3ProjectMuse(_) => Self::XML_MIME_TYPE, + MetadataSpecification::Onix3Oapen(_) => Self::XML_MIME_TYPE, MetadataSpecification::CsvThoth(_) => Self::CSV_MIME_TYPE, } } @@ -52,6 +54,7 @@ where fn file_name(&self) -> String { match &self.specification { MetadataSpecification::Onix3ProjectMuse(_) => self.xml_file_name(), + MetadataSpecification::Onix3Oapen(_) => self.xml_file_name(), MetadataSpecification::CsvThoth(_) => self.csv_file_name(), } } @@ -84,6 +87,7 @@ impl MetadataRecord> { MetadataSpecification::Onix3ProjectMuse(onix3_project_muse) => { onix3_project_muse.generate(&self.data) } + MetadataSpecification::Onix3Oapen(onix3_oapen) => onix3_oapen.generate(&self.data), MetadataSpecification::CsvThoth(csv_thoth) => csv_thoth.generate(&self.data), } } @@ -134,6 +138,7 @@ impl FromStr for MetadataSpecification { "onix_3.0::project_muse" => { Ok(MetadataSpecification::Onix3ProjectMuse(Onix3ProjectMuse {})) } + "onix_3.0::oapen" => Ok(MetadataSpecification::Onix3Oapen(Onix3Oapen {})), "csv::thoth" => Ok(MetadataSpecification::CsvThoth(CsvThoth {})), _ => Err(ThothError::InvalidMetadataSpecification(input.to_string())), } @@ -144,6 +149,7 @@ impl ToString for MetadataSpecification { fn to_string(&self) -> String { match self { MetadataSpecification::Onix3ProjectMuse(_) => "onix_3.0::project_muse".to_string(), + MetadataSpecification::Onix3Oapen(_) => "onix_3.0::oapen".to_string(), MetadataSpecification::CsvThoth(_) => "csv::thoth".to_string(), } } @@ -181,6 +187,15 @@ mod tests { assert_eq!( to_test.file_name(), "onix_3.0__project_muse__some_id.xml".to_string() - ) + ); + let to_test = MetadataRecord::new( + "some_id".to_string(), + MetadataSpecification::Onix3Oapen(Onix3Oapen {}), + vec![], + ); + assert_eq!( + to_test.file_name(), + "onix_3.0__oapen__some_id.xml".to_string() + ); } } diff --git a/thoth-export-server/src/xml/mod.rs b/thoth-export-server/src/xml/mod.rs index 265df9e4..c7a2da35 100644 --- a/thoth-export-server/src/xml/mod.rs +++ b/thoth-export-server/src/xml/mod.rs @@ -77,3 +77,5 @@ pub(crate) trait XmlElementBlock { mod onix3_project_muse; pub(crate) use onix3_project_muse::Onix3ProjectMuse; +mod onix3_oapen; +pub(crate) use onix3_oapen::Onix3Oapen; diff --git a/thoth-export-server/src/xml/onix3_oapen.rs b/thoth-export-server/src/xml/onix3_oapen.rs new file mode 100644 index 00000000..8c727ee1 --- /dev/null +++ b/thoth-export-server/src/xml/onix3_oapen.rs @@ -0,0 +1,522 @@ +use chrono::Utc; +use std::collections::HashMap; +use std::io::Write; +use thoth_client::{ + ContributionType, LanguageRelation, PublicationType, SubjectType, Work, WorkContributions, + WorkLanguages, WorkPublications, WorkStatus, +}; +use xml::writer::{EventWriter, XmlEvent}; + +use super::{write_element_block, XmlElement, XmlSpecification}; +use crate::xml::{write_full_element_block, XmlElementBlock}; +use thoth_api::model::DOI_DOMAIN; +use thoth_errors::{ThothError, ThothResult}; + +pub struct Onix3Oapen {} + +impl XmlSpecification for Onix3Oapen { + fn handle_event(w: &mut EventWriter, works: &[Work]) -> ThothResult<()> { + let mut attr_map: HashMap<&str, &str> = HashMap::new(); + + attr_map.insert("release", "3.0"); + attr_map.insert("xmlns", "http://ns.editeur.org/onix/3.0/reference"); + + write_full_element_block("ONIXMessage", None, Some(attr_map), w, |w| { + write_element_block("Header", w, |w| { + write_element_block("Sender", w, |w| { + write_element_block("SenderName", w, |w| { + w.write(XmlEvent::Characters("Thoth")).map_err(|e| e.into()) + })?; + write_element_block("EmailAddress", w, |w| { + w.write(XmlEvent::Characters("info@thoth.pub")) + .map_err(|e| e.into()) + }) + })?; + write_element_block("SentDateTime", w, |w| { + w.write(XmlEvent::Characters( + &Utc::now().format("%Y%m%dT%H%M%S").to_string(), + )) + .map_err(|e| e.into()) + }) + })?; + + match works.len() { + 0 => Err(ThothError::IncompleteMetadataRecord( + "onix_3.0::oapen".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 can only generate the document if there's a PDF + if let Some(pdf_url) = self + .publications + .iter() + .find(|p| p.publication_type.eq(&PublicationType::PDF)) + .and_then(|p| p.publication_url.as_ref()) + { + 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.replace(DOI_DOMAIN, ""))) + .map_err(|e| e.into()) + }) + })?; + } + write_element_block("DescriptiveDetail", w, |w| { + // 00 Single-component retail product + write_element_block("ProductComposition", w, |w| { + w.write(XmlEvent::Characters("00")).map_err(|e| e.into()) + })?; + // EB Digital download and online + write_element_block("ProductForm", w, |w| { + w.write(XmlEvent::Characters("EB")).map_err(|e| e.into()) + })?; + // E107 PDF + write_element_block("ProductFormDetail", w, |w| { + w.write(XmlEvent::Characters("E107")).map_err(|e| e.into()) + })?; + // 10 Text (eye-readable) + write_element_block("PrimaryContentType", w, |w| { + w.write(XmlEvent::Characters("10")).map_err(|e| e.into()) + })?; + if let Some(license) = &self.license { + write_element_block("EpubLicense", w, |w| { + write_element_block("EpubLicenseName", w, |w| { + w.write(XmlEvent::Characters("Creative Commons License")) + .map_err(|e| e.into()) + })?; + write_element_block("EpubLicenseExpression", w, |w| { + write_element_block("EpubLicenseExpressionType", w, |w| { + w.write(XmlEvent::Characters("02")).map_err(|e| e.into()) + })?; + write_element_block("EpubLicenseExpressionLink", w, |w| { + w.write(XmlEvent::Characters(&license)) + .map_err(|e| e.into()) + }) + }) + })?; + } + write_element_block("TitleDetail", w, |w| { + // 01 Distinctive title (book) + write_element_block("TitleType", w, |w| { + w.write(XmlEvent::Characters("01")).map_err(|e| e.into()) + })?; + write_element_block("TitleElement", w, |w| { + // 01 Product + write_element_block("TitleElementLevel", 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()) + }) + } + }) + })?; + XmlElementBlock::::xml_element(&self.contributions, 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 { + write_element_block("Subject", w, |w| { + XmlElement::::xml_element(&subject.subject_type, w)?; + write_element_block("SubjectCode", w, |w| { + w.write(XmlEvent::Characters(&subject.subject_code)) + .map_err(|e| e.into()) + }) + })?; + } + Ok(()) + })?; + if self.long_abstract.is_some() || self.toc.is_some() { + write_element_block("CollateralDetail", w, |w| { + if let Some(labstract) = &self.long_abstract { + write_element_block("TextContent", w, |w| { + let mut lang_fmt: HashMap<&str, &str> = HashMap::new(); + lang_fmt.insert("language", "eng"); + // 03 Description ("30 Abstract" not implemented in OAPEN) + write_element_block("TextType", w, |w| { + w.write(XmlEvent::Characters("03")).map_err(|e| e.into()) + })?; + // 00 Unrestricted + write_element_block("ContentAudience", w, |w| { + w.write(XmlEvent::Characters("00")).map_err(|e| e.into()) + })?; + write_full_element_block("Text", None, Some(lang_fmt), w, |w| { + w.write(XmlEvent::Characters(&labstract)) + .map_err(|e| e.into()) + }) + })?; + } + if let Some(toc) = &self.toc { + write_element_block("TextContent", w, |w| { + // 04 Table of contents + write_element_block("TextType", w, |w| { + w.write(XmlEvent::Characters("04")).map_err(|e| e.into()) + })?; + // 00 Unrestricted + write_element_block("ContentAudience", w, |w| { + w.write(XmlEvent::Characters("00")).map_err(|e| e.into()) + })?; + write_element_block("Text", w, |w| { + w.write(XmlEvent::Characters(&toc)).map_err(|e| e.into()) + }) + })?; + } + Ok(()) + })?; + } + write_element_block("PublishingDetail", w, |w| { + 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(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("PublishingDate", w, |w| { + let mut date_fmt: HashMap<&str, &str> = HashMap::new(); + date_fmt.insert("dateformat", "01"); // 01 YYYYMM + + write_element_block("PublishingDateRole", w, |w| { + // 19 Publication date of print counterpart + w.write(XmlEvent::Characters("19")).map_err(|e| e.into()) + })?; + // dateformat="01" YYYYMM + write_full_element_block("Date", None, Some(date_fmt), w, |w| { + w.write(XmlEvent::Characters(&date.format("%Y%m").to_string())) + .map_err(|e| e.into()) + }) + })?; + } + Ok(()) + })?; + if !isbns.is_empty() { + write_element_block("RelatedMaterial", w, |w| { + for isbn in &isbns { + write_element_block("RelatedProduct", w, |w| { + // 06 Alternative format + write_element_block("ProductRelationCode", w, |w| { + w.write(XmlEvent::Characters("06")).map_err(|e| e.into()) + })?; + write_element_block("ProductIdentifier", w, |w| { + // 06 ISBN + 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(&isbn)).map_err(|e| e.into()) + }) + }) + })?; + } + Ok(()) + })?; + } + write_element_block("ProductSupply", w, |w| { + let mut supplies: HashMap = HashMap::new(); + supplies.insert( + pdf_url.to_string(), + "Publisher's website: download the title".to_string(), + ); + if let Some(landing_page) = &self.landing_page { + supplies.insert( + landing_page.to_string(), + "Publisher's website: web shop".to_string(), + ); + } + for (url, description) in supplies.iter() { + write_element_block("SupplyDetail", w, |w| { + write_element_block("Supplier", w, |w| { + // 09 Publisher to end-customers + write_element_block("SupplierRole", w, |w| { + w.write(XmlEvent::Characters("11")).map_err(|e| e.into()) + })?; + write_element_block("SupplierName", w, |w| { + w.write(XmlEvent::Characters( + &self.imprint.publisher.publisher_name, + )) + .map_err(|e| e.into()) + })?; + write_element_block("Website", w, |w| { + // 01 Publisher’s corporate website + write_element_block("WebsiteRole", w, |w| { + w.write(XmlEvent::Characters("01")).map_err(|e| e.into()) + })?; + write_element_block("WebsiteDescription", w, |w| { + w.write(XmlEvent::Characters(&description)) + .map_err(|e| e.into()) + })?; + write_element_block("WebsiteLink", w, |w| { + w.write(XmlEvent::Characters(&url)).map_err(|e| e.into()) + }) + }) + })?; + // 99 Contact supplier + write_element_block("ProductAvailability", w, |w| { + w.write(XmlEvent::Characters("99")).map_err(|e| e.into()) + })?; + // 04 Contact supplier + write_element_block("UnpricedItemType", w, |w| { + w.write(XmlEvent::Characters("04")).map_err(|e| e.into()) + }) + })?; + } + Ok(()) + }) + }) + } else { + Err(ThothError::IncompleteMetadataRecord( + "onix_3.0::oapen".to_string(), + "Missing PDF 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 { + 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!(), + } + } +} + +// Replace with implementation for WorkContributions (without the vector) +// when we implement contribution ordering +impl XmlElementBlock for Vec { + fn xml_element(&self, w: &mut EventWriter) -> ThothResult<()> { + for (mut sequence_number, contribution) in self.iter().enumerate() { + sequence_number += 1; + write_element_block("Contributor", w, |w| { + write_element_block("SequenceNumber", w, |w| { + w.write(XmlEvent::Characters(&sequence_number.to_string())) + .map_err(|e| e.into()) + })?; + XmlElement::::xml_element(&contribution.contribution_type, w)?; + + if let Some(orcid) = &contribution.contributor.orcid { + write_element_block("NameIdentifier", w, |w| { + write_element_block("NameIDType", w, |w| { + w.write(XmlEvent::Characters("21")).map_err(|e| e.into()) + })?; + write_element_block("IDValue", w, |w| { + w.write(XmlEvent::Characters(&orcid)).map_err(|e| e.into()) + }) + })?; + } + if let Some(first_name) = &contribution.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(&contribution.last_name)) + .map_err(|e| e.into()) + })?; + } else { + write_element_block("PersonName", w, |w| { + w.write(XmlEvent::Characters(&contribution.full_name)) + .map_err(|e| e.into()) + })?; + } + Ok(()) + })?; + } + 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()) + }) + }) + } +} From dcd66af0da764a7445555688cc3337d9b423a441 Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Fri, 16 Jul 2021 16:08:53 +0100 Subject: [PATCH 04/14] Add Issue/Audience/Cover/Funding blocks and adjust existing blocks to match example file/comments as far as possible --- thoth-export-server/src/xml/onix3_oapen.rs | 145 ++++++++++++++++++--- 1 file changed, 129 insertions(+), 16 deletions(-) diff --git a/thoth-export-server/src/xml/onix3_oapen.rs b/thoth-export-server/src/xml/onix3_oapen.rs index 8c727ee1..02fde986 100644 --- a/thoth-export-server/src/xml/onix3_oapen.rs +++ b/thoth-export-server/src/xml/onix3_oapen.rs @@ -34,7 +34,7 @@ impl XmlSpecification for Onix3Oapen { })?; write_element_block("SentDateTime", w, |w| { w.write(XmlEvent::Characters( - &Utc::now().format("%Y%m%dT%H%M%S").to_string(), + &Utc::now().format("%Y%m%d").to_string(), )) .map_err(|e| e.into()) }) @@ -146,6 +146,36 @@ impl XmlElementBlock for Work { }) })?; } + for issue in &self.issues { + write_element_block("Collection", w, |w| { + // 10 Publisher collection (e.g. series) + write_element_block("CollectionType", w, |w| { + w.write(XmlEvent::Characters("10")).map_err(|e| e.into()) + })?; + write_element_block("TitleDetail", w, |w| { + // 01 Cover title (serial) + write_element_block("TitleType", w, |w| { + w.write(XmlEvent::Characters("01")).map_err(|e| e.into()) + })?; + write_element_block("TitleElement", w, |w| { + // 02 Collection level + write_element_block("TitleElementLevel", w, |w| { + w.write(XmlEvent::Characters("02")).map_err(|e| e.into()) + })?; + write_element_block("PartNumber", w, |w| { + w.write(XmlEvent::Characters( + &issue.issue_ordinal.to_string(), + )) + .map_err(|e| e.into()) + })?; + write_element_block("TitleText", w, |w| { + w.write(XmlEvent::Characters(&issue.series.series_name)) + .map_err(|e| e.into()) + }) + }) + }) + })?; + } write_element_block("TitleDetail", w, |w| { // 01 Distinctive title (book) write_element_block("TitleType", w, |w| { @@ -196,12 +226,30 @@ impl XmlElementBlock for Work { for subject in &self.subjects { write_element_block("Subject", w, |w| { XmlElement::::xml_element(&subject.subject_type, w)?; - write_element_block("SubjectCode", w, |w| { - w.write(XmlEvent::Characters(&subject.subject_code)) - .map_err(|e| e.into()) - }) + match subject.subject_type { + SubjectType::KEYWORD | SubjectType::CUSTOM => { + write_element_block("SubjectHeadingText", w, |w| { + w.write(XmlEvent::Characters(&subject.subject_code)) + .map_err(|e| e.into()) + }) + } + _ => write_element_block("SubjectCode", w, |w| { + w.write(XmlEvent::Characters(&subject.subject_code)) + .map_err(|e| e.into()) + }), + } })?; } + 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()) + }) + })?; Ok(()) })?; if self.long_abstract.is_some() || self.toc.is_some() { @@ -239,6 +287,32 @@ impl XmlElementBlock for Work { }) })?; } + if let Some(cover_url) = &self.cover_url { + write_element_block("SupportingResource", w, |w| { + // 01 Front cover + write_element_block("ResourceContentType", w, |w| { + w.write(XmlEvent::Characters("01")).map_err(|e| e.into()) + })?; + // 00 Unrestricted + write_element_block("ContentAudience", w, |w| { + w.write(XmlEvent::Characters("00")).map_err(|e| e.into()) + })?; + // 03 Image + write_element_block("ResourceMode", w, |w| { + w.write(XmlEvent::Characters("03")).map_err(|e| e.into()) + })?; + write_element_block("ResourceVersion", w, |w| { + // 02 Downloadable file + write_element_block("ResourceForm", w, |w| { + w.write(XmlEvent::Characters("02")).map_err(|e| e.into()) + })?; + write_element_block("ResourceLink", w, |w| { + w.write(XmlEvent::Characters(cover_url)) + .map_err(|e| e.into()) + }) + }) + })?; + } Ok(()) })?; } @@ -259,6 +333,38 @@ impl XmlElementBlock for Work { .map_err(|e| e.into()) }) })?; + for funding in &self.fundings { + write_element_block("Publisher", w, |w| { + // 16 Funding body + write_element_block("PublishingRole", w, |w| { + w.write(XmlEvent::Characters("16")).map_err(|e| e.into()) + })?; + write_element_block("PublisherName", w, |w| { + w.write(XmlEvent::Characters(&funding.funder.funder_name)) + .map_err(|e| e.into()) + })?; + if let Some(program) = &funding.program { + write_element_block("Funding", w, |w| { + write_element_block("FundingIdentifier", w, |w| { + // 01 Proprietary + write_element_block("FundingIDType", w, |w| { + w.write(XmlEvent::Characters("01")) + .map_err(|e| e.into()) + })?; + write_element_block("IDTypeName", w, |w| { + w.write(XmlEvent::Characters("program")) + .map_err(|e| e.into()) + })?; + write_element_block("IDValue", w, |w| { + w.write(XmlEvent::Characters(program)) + .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()) @@ -268,15 +374,15 @@ impl XmlElementBlock for Work { if let Some(date) = self.publication_date { write_element_block("PublishingDate", w, |w| { let mut date_fmt: HashMap<&str, &str> = HashMap::new(); - date_fmt.insert("dateformat", "01"); // 01 YYYYMM + date_fmt.insert("dateformat", "05"); // 01 YYYY write_element_block("PublishingDateRole", w, |w| { // 19 Publication date of print counterpart w.write(XmlEvent::Characters("19")).map_err(|e| e.into()) })?; - // dateformat="01" YYYYMM + // dateformat="05" YYYY write_full_element_block("Date", None, Some(date_fmt), w, |w| { - w.write(XmlEvent::Characters(&date.format("%Y%m").to_string())) + w.write(XmlEvent::Characters(&date.format("%Y").to_string())) .map_err(|e| e.into()) }) })?; @@ -292,9 +398,9 @@ impl XmlElementBlock for Work { w.write(XmlEvent::Characters("06")).map_err(|e| e.into()) })?; write_element_block("ProductIdentifier", w, |w| { - // 06 ISBN + // 15 ISBN-13 write_element_block("ProductIDType", w, |w| { - w.write(XmlEvent::Characters("06")).map_err(|e| e.into()) + 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()) @@ -306,15 +412,21 @@ impl XmlElementBlock for Work { })?; } write_element_block("ProductSupply", w, |w| { - let mut supplies: HashMap = HashMap::new(); + let mut supplies: HashMap = HashMap::new(); supplies.insert( pdf_url.to_string(), - "Publisher's website: download the title".to_string(), + ( + "29".to_string(), + "Publisher's website: download the title".to_string(), + ), ); if let Some(landing_page) = &self.landing_page { supplies.insert( landing_page.to_string(), - "Publisher's website: web shop".to_string(), + ( + "01".to_string(), + "Publisher's website: web shop".to_string(), + ), ); } for (url, description) in supplies.iter() { @@ -322,7 +434,7 @@ impl XmlElementBlock for Work { write_element_block("Supplier", w, |w| { // 09 Publisher to end-customers write_element_block("SupplierRole", w, |w| { - w.write(XmlEvent::Characters("11")).map_err(|e| e.into()) + w.write(XmlEvent::Characters("09")).map_err(|e| e.into()) })?; write_element_block("SupplierName", w, |w| { w.write(XmlEvent::Characters( @@ -333,10 +445,11 @@ impl XmlElementBlock for Work { write_element_block("Website", w, |w| { // 01 Publisher’s corporate website write_element_block("WebsiteRole", w, |w| { - w.write(XmlEvent::Characters("01")).map_err(|e| e.into()) + w.write(XmlEvent::Characters(&description.0)) + .map_err(|e| e.into()) })?; write_element_block("WebsiteDescription", w, |w| { - w.write(XmlEvent::Characters(&description)) + w.write(XmlEvent::Characters(&description.1)) .map_err(|e| e.into()) })?; write_element_block("WebsiteLink", w, |w| { From 8b6789de10874751f8ed763abde8de36010bbcec Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Wed, 21 Jul 2021 16:10:22 +0100 Subject: [PATCH 05/14] Adjust OAPEN output based on feedback (ignore Custom subjects/TOC, extend funding identifiers, simplify contributor roles) --- thoth-export-server/src/xml/onix3_oapen.rs | 106 +++++++++++---------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/thoth-export-server/src/xml/onix3_oapen.rs b/thoth-export-server/src/xml/onix3_oapen.rs index 02fde986..2baff202 100644 --- a/thoth-export-server/src/xml/onix3_oapen.rs +++ b/thoth-export-server/src/xml/onix3_oapen.rs @@ -224,21 +224,25 @@ impl XmlElementBlock for Work { })?; } for subject in &self.subjects { - write_element_block("Subject", w, |w| { - XmlElement::::xml_element(&subject.subject_type, w)?; - match subject.subject_type { - SubjectType::KEYWORD | SubjectType::CUSTOM => { - write_element_block("SubjectHeadingText", w, |w| { + // Don't output Custom codes, as these are not imported by OAPEN, + // and only used for internal purposes + if subject.subject_type != SubjectType::CUSTOM { + write_element_block("Subject", w, |w| { + XmlElement::::xml_element(&subject.subject_type, w)?; + match subject.subject_type { + SubjectType::KEYWORD => { + write_element_block("SubjectHeadingText", w, |w| { + w.write(XmlEvent::Characters(&subject.subject_code)) + .map_err(|e| e.into()) + }) + } + _ => write_element_block("SubjectCode", w, |w| { w.write(XmlEvent::Characters(&subject.subject_code)) .map_err(|e| e.into()) - }) + }), } - _ => write_element_block("SubjectCode", w, |w| { - w.write(XmlEvent::Characters(&subject.subject_code)) - .map_err(|e| e.into()) - }), - } - })?; + })?; + } } write_element_block("Audience", w, |w| { // 01 ONIX audience codes @@ -272,21 +276,6 @@ impl XmlElementBlock for Work { }) })?; } - if let Some(toc) = &self.toc { - write_element_block("TextContent", w, |w| { - // 04 Table of contents - write_element_block("TextType", w, |w| { - w.write(XmlEvent::Characters("04")).map_err(|e| e.into()) - })?; - // 00 Unrestricted - write_element_block("ContentAudience", w, |w| { - w.write(XmlEvent::Characters("00")).map_err(|e| e.into()) - })?; - write_element_block("Text", w, |w| { - w.write(XmlEvent::Characters(&toc)).map_err(|e| e.into()) - }) - })?; - } if let Some(cover_url) = &self.cover_url { write_element_block("SupportingResource", w, |w| { // 01 Front cover @@ -343,23 +332,38 @@ impl XmlElementBlock for Work { w.write(XmlEvent::Characters(&funding.funder.funder_name)) .map_err(|e| e.into()) })?; + let mut identifiers: HashMap = HashMap::new(); if let Some(program) = &funding.program { + identifiers.insert("programname".to_string(), program.to_string()); + } + if let Some(project_name) = &funding.project_name { + identifiers + .insert("projectname".to_string(), project_name.to_string()); + } + if let Some(grant_number) = &funding.grant_number { + identifiers + .insert("grantnumber".to_string(), grant_number.to_string()); + } + if !identifiers.is_empty() { write_element_block("Funding", w, |w| { - write_element_block("FundingIdentifier", w, |w| { - // 01 Proprietary - write_element_block("FundingIDType", w, |w| { - w.write(XmlEvent::Characters("01")) - .map_err(|e| e.into()) - })?; - write_element_block("IDTypeName", w, |w| { - w.write(XmlEvent::Characters("program")) - .map_err(|e| e.into()) + for (typename, value) in &identifiers { + write_element_block("FundingIdentifier", w, |w| { + // 01 Proprietary + write_element_block("FundingIDType", w, |w| { + w.write(XmlEvent::Characters("01")) + .map_err(|e| e.into()) + })?; + write_element_block("IDTypeName", w, |w| { + w.write(XmlEvent::Characters(&typename)) + .map_err(|e| e.into()) + })?; + write_element_block("IDValue", w, |w| { + w.write(XmlEvent::Characters(&value)) + .map_err(|e| e.into()) + }) })?; - write_element_block("IDValue", w, |w| { - w.write(XmlEvent::Characters(program)) - .map_err(|e| e.into()) - }) - }) + } + Ok(()) })?; } Ok(()) @@ -534,8 +538,8 @@ impl XmlElement for SubjectType { SubjectType::KEYWORD => "20", SubjectType::LCC => "04", SubjectType::THEMA => "93", - SubjectType::CUSTOM => "B2", - SubjectType::Other(_) => unreachable!(), + // Custom codes are not output for OAPEN + SubjectType::CUSTOM | SubjectType::Other(_) => unreachable!(), } } } @@ -560,14 +564,14 @@ impl XmlElement for ContributionType { 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::TRANSLATOR + | ContributionType::PHOTOGRAPHER + | ContributionType::ILUSTRATOR + | ContributionType::MUSIC_EDITOR + | ContributionType::FOREWORD_BY + | ContributionType::INTRODUCTION_BY + | ContributionType::AFTERWORD_BY + | ContributionType::PREFACE_BY => "Z01", ContributionType::Other(_) => unreachable!(), } } From ad1bea3aae9eda965941156b27ed66cdb1d75907 Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Wed, 21 Jul 2021 17:21:07 +0100 Subject: [PATCH 06/14] Pull out Issue and Funding loop logic into separate Impl blocks (output confirmed unchanged) --- thoth-export-server/src/xml/onix3_oapen.rs | 154 +++++++++++---------- 1 file changed, 80 insertions(+), 74 deletions(-) diff --git a/thoth-export-server/src/xml/onix3_oapen.rs b/thoth-export-server/src/xml/onix3_oapen.rs index 2baff202..e8e6bfd6 100644 --- a/thoth-export-server/src/xml/onix3_oapen.rs +++ b/thoth-export-server/src/xml/onix3_oapen.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::io::Write; use thoth_client::{ ContributionType, LanguageRelation, PublicationType, SubjectType, Work, WorkContributions, - WorkLanguages, WorkPublications, WorkStatus, + WorkFundings, WorkIssues, WorkLanguages, WorkPublications, WorkStatus, }; use xml::writer::{EventWriter, XmlEvent}; @@ -147,34 +147,7 @@ impl XmlElementBlock for Work { })?; } for issue in &self.issues { - write_element_block("Collection", w, |w| { - // 10 Publisher collection (e.g. series) - write_element_block("CollectionType", w, |w| { - w.write(XmlEvent::Characters("10")).map_err(|e| e.into()) - })?; - write_element_block("TitleDetail", w, |w| { - // 01 Cover title (serial) - write_element_block("TitleType", w, |w| { - w.write(XmlEvent::Characters("01")).map_err(|e| e.into()) - })?; - write_element_block("TitleElement", w, |w| { - // 02 Collection level - write_element_block("TitleElementLevel", w, |w| { - w.write(XmlEvent::Characters("02")).map_err(|e| e.into()) - })?; - write_element_block("PartNumber", w, |w| { - w.write(XmlEvent::Characters( - &issue.issue_ordinal.to_string(), - )) - .map_err(|e| e.into()) - })?; - write_element_block("TitleText", w, |w| { - w.write(XmlEvent::Characters(&issue.series.series_name)) - .map_err(|e| e.into()) - }) - }) - }) - })?; + XmlElementBlock::::xml_element(issue, w).ok(); } write_element_block("TitleDetail", w, |w| { // 01 Distinctive title (book) @@ -323,51 +296,7 @@ impl XmlElementBlock for Work { }) })?; for funding in &self.fundings { - write_element_block("Publisher", w, |w| { - // 16 Funding body - write_element_block("PublishingRole", w, |w| { - w.write(XmlEvent::Characters("16")).map_err(|e| e.into()) - })?; - write_element_block("PublisherName", w, |w| { - w.write(XmlEvent::Characters(&funding.funder.funder_name)) - .map_err(|e| e.into()) - })?; - let mut identifiers: HashMap = HashMap::new(); - if let Some(program) = &funding.program { - identifiers.insert("programname".to_string(), program.to_string()); - } - if let Some(project_name) = &funding.project_name { - identifiers - .insert("projectname".to_string(), project_name.to_string()); - } - if let Some(grant_number) = &funding.grant_number { - identifiers - .insert("grantnumber".to_string(), grant_number.to_string()); - } - if !identifiers.is_empty() { - write_element_block("Funding", w, |w| { - for (typename, value) in &identifiers { - write_element_block("FundingIdentifier", w, |w| { - // 01 Proprietary - write_element_block("FundingIDType", w, |w| { - w.write(XmlEvent::Characters("01")) - .map_err(|e| e.into()) - })?; - write_element_block("IDTypeName", w, |w| { - w.write(XmlEvent::Characters(&typename)) - .map_err(|e| e.into()) - })?; - write_element_block("IDValue", w, |w| { - w.write(XmlEvent::Characters(&value)) - .map_err(|e| e.into()) - }) - })?; - } - Ok(()) - })?; - } - Ok(()) - })?; + XmlElementBlock::::xml_element(funding, w).ok(); } if let Some(place) = &self.place { write_element_block("CityOfPublication", w, |w| { @@ -637,3 +566,80 @@ impl XmlElementBlock for WorkLanguages { }) } } + +impl XmlElementBlock for WorkIssues { + fn xml_element(&self, w: &mut EventWriter) -> ThothResult<()> { + write_element_block("Collection", w, |w| { + // 10 Publisher collection (e.g. series) + write_element_block("CollectionType", w, |w| { + w.write(XmlEvent::Characters("10")).map_err(|e| e.into()) + })?; + write_element_block("TitleDetail", w, |w| { + // 01 Cover title (serial) + write_element_block("TitleType", w, |w| { + w.write(XmlEvent::Characters("01")).map_err(|e| e.into()) + })?; + write_element_block("TitleElement", w, |w| { + // 02 Collection level + write_element_block("TitleElementLevel", w, |w| { + w.write(XmlEvent::Characters("02")).map_err(|e| e.into()) + })?; + write_element_block("PartNumber", w, |w| { + w.write(XmlEvent::Characters(&self.issue_ordinal.to_string())) + .map_err(|e| e.into()) + })?; + write_element_block("TitleText", w, |w| { + w.write(XmlEvent::Characters(&self.series.series_name)) + .map_err(|e| e.into()) + }) + }) + }) + }) + } +} + +impl XmlElementBlock for WorkFundings { + fn xml_element(&self, w: &mut EventWriter) -> ThothResult<()> { + write_element_block("Publisher", w, |w| { + // 16 Funding body + write_element_block("PublishingRole", w, |w| { + w.write(XmlEvent::Characters("16")).map_err(|e| e.into()) + })?; + write_element_block("PublisherName", w, |w| { + w.write(XmlEvent::Characters(&self.funder.funder_name)) + .map_err(|e| e.into()) + })?; + let mut identifiers: HashMap = HashMap::new(); + if let Some(program) = &self.program { + identifiers.insert("programname".to_string(), program.to_string()); + } + if let Some(project_name) = &self.project_name { + identifiers.insert("projectname".to_string(), project_name.to_string()); + } + if let Some(grant_number) = &self.grant_number { + identifiers.insert("grantnumber".to_string(), grant_number.to_string()); + } + if !identifiers.is_empty() { + write_element_block("Funding", w, |w| { + for (typename, value) in &identifiers { + write_element_block("FundingIdentifier", w, |w| { + // 01 Proprietary + write_element_block("FundingIDType", w, |w| { + w.write(XmlEvent::Characters("01")).map_err(|e| e.into()) + })?; + write_element_block("IDTypeName", w, |w| { + w.write(XmlEvent::Characters(&typename)) + .map_err(|e| e.into()) + })?; + write_element_block("IDValue", w, |w| { + w.write(XmlEvent::Characters(&value)).map_err(|e| e.into()) + }) + })?; + } + Ok(()) + })?; + } + Ok(()) + }) + } +} From 752e51dd7e122dcd939c1038684d5c60367f73ee Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Thu, 22 Jul 2021 15:40:46 +0100 Subject: [PATCH 07/14] Write out contribution SequenceNumber from ContributionOrdinal instead of looping (missed under PR #261) --- thoth-client/assets/queries.graphql | 1 + thoth-client/assets/schema.json | 16 ++++ thoth-export-server/src/csv/csv_thoth.rs | 2 + thoth-export-server/src/xml/onix3_oapen.rs | 76 +++++++++---------- .../src/xml/onix3_project_muse.rs | 76 +++++++++---------- 5 files changed, 91 insertions(+), 80 deletions(-) diff --git a/thoth-client/assets/queries.graphql b/thoth-client/assets/queries.graphql index 1502ddfe..a103c1fd 100644 --- a/thoth-client/assets/queries.graphql +++ b/thoth-client/assets/queries.graphql @@ -52,6 +52,7 @@ fragment Work on Work { mainContribution biography institution + contributionOrdinal contributor { orcid } diff --git a/thoth-client/assets/schema.json b/thoth-client/assets/schema.json index f170a158..c673ad04 100644 --- a/thoth-client/assets/schema.json +++ b/thoth-client/assets/schema.json @@ -5632,6 +5632,22 @@ } } }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "contributionOrdinal", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + }, { "args": [], "deprecationReason": null, diff --git a/thoth-export-server/src/csv/csv_thoth.rs b/thoth-export-server/src/csv/csv_thoth.rs index 8cbc2297..5b9507cf 100644 --- a/thoth-export-server/src/csv/csv_thoth.rs +++ b/thoth-export-server/src/csv/csv_thoth.rs @@ -359,6 +359,7 @@ mod tests { main_contribution: true, biography: None, institution: None, + contribution_ordinal: 1, contributor: WorkContributionsContributor { orcid: Some("https://orcid.org/0000-0000-0000-0001".to_string()), }, @@ -371,6 +372,7 @@ mod tests { main_contribution: true, biography: None, institution: None, + contribution_ordinal: 2, contributor: WorkContributionsContributor { orcid: None, }, diff --git a/thoth-export-server/src/xml/onix3_oapen.rs b/thoth-export-server/src/xml/onix3_oapen.rs index e8e6bfd6..f066aa57 100644 --- a/thoth-export-server/src/xml/onix3_oapen.rs +++ b/thoth-export-server/src/xml/onix3_oapen.rs @@ -176,7 +176,9 @@ impl XmlElementBlock for Work { } }) })?; - XmlElementBlock::::xml_element(&self.contributions, w).ok(); + for contribution in &self.contributions { + XmlElementBlock::::xml_element(contribution, w).ok(); + } for language in &self.languages { XmlElementBlock::::xml_element(language, w).ok(); } @@ -506,48 +508,42 @@ impl XmlElement for ContributionType { } } -// Replace with implementation for WorkContributions (without the vector) -// when we implement contribution ordering -impl XmlElementBlock for Vec { +impl XmlElementBlock for WorkContributions { fn xml_element(&self, w: &mut EventWriter) -> ThothResult<()> { - for (mut sequence_number, contribution) in self.iter().enumerate() { - sequence_number += 1; - write_element_block("Contributor", w, |w| { - write_element_block("SequenceNumber", w, |w| { - w.write(XmlEvent::Characters(&sequence_number.to_string())) - .map_err(|e| e.into()) - })?; - XmlElement::::xml_element(&contribution.contribution_type, w)?; + 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) = &contribution.contributor.orcid { - write_element_block("NameIdentifier", w, |w| { - write_element_block("NameIDType", w, |w| { - w.write(XmlEvent::Characters("21")).map_err(|e| e.into()) - })?; - write_element_block("IDValue", w, |w| { - w.write(XmlEvent::Characters(&orcid)).map_err(|e| e.into()) - }) + if let Some(orcid) = &self.contributor.orcid { + write_element_block("NameIdentifier", w, |w| { + write_element_block("NameIDType", w, |w| { + w.write(XmlEvent::Characters("21")).map_err(|e| e.into()) })?; - } - if let Some(first_name) = &contribution.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(&contribution.last_name)) - .map_err(|e| e.into()) - })?; - } else { - write_element_block("PersonName", w, |w| { - w.write(XmlEvent::Characters(&contribution.full_name)) - .map_err(|e| e.into()) - })?; - } - Ok(()) - })?; - } - Ok(()) + write_element_block("IDValue", w, |w| { + w.write(XmlEvent::Characters(&orcid)).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(()) + }) } } diff --git a/thoth-export-server/src/xml/onix3_project_muse.rs b/thoth-export-server/src/xml/onix3_project_muse.rs index 179e9056..8ba0f31a 100644 --- a/thoth-export-server/src/xml/onix3_project_muse.rs +++ b/thoth-export-server/src/xml/onix3_project_muse.rs @@ -173,7 +173,9 @@ impl XmlElementBlock for Work { } }) })?; - XmlElementBlock::::xml_element(&self.contributions, w).ok(); + for contribution in &self.contributions { + XmlElementBlock::::xml_element(contribution, w).ok(); + } for language in &self.languages { XmlElementBlock::::xml_element(language, w).ok(); } @@ -460,48 +462,42 @@ impl XmlElement for ContributionType { } } -// Replace with implementation for WorkContributions (without the vector) -// when we implement contribution ordering -impl XmlElementBlock for Vec { +impl XmlElementBlock for WorkContributions { fn xml_element(&self, w: &mut EventWriter) -> ThothResult<()> { - for (mut sequence_number, contribution) in self.iter().enumerate() { - sequence_number += 1; - write_element_block("Contributor", w, |w| { - write_element_block("SequenceNumber", w, |w| { - w.write(XmlEvent::Characters(&sequence_number.to_string())) - .map_err(|e| e.into()) - })?; - XmlElement::::xml_element(&contribution.contribution_type, w)?; + 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) = &contribution.contributor.orcid { - write_element_block("NameIdentifier", w, |w| { - write_element_block("NameIDType", w, |w| { - w.write(XmlEvent::Characters("21")).map_err(|e| e.into()) - })?; - write_element_block("IDValue", w, |w| { - w.write(XmlEvent::Characters(&orcid)).map_err(|e| e.into()) - }) - })?; - } - if let Some(first_name) = &contribution.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(&contribution.last_name)) - .map_err(|e| e.into()) - })?; - } else { - write_element_block("PersonName", w, |w| { - w.write(XmlEvent::Characters(&contribution.full_name)) - .map_err(|e| e.into()) + if let Some(orcid) = &self.contributor.orcid { + write_element_block("NameIdentifier", w, |w| { + write_element_block("NameIDType", w, |w| { + w.write(XmlEvent::Characters("21")).map_err(|e| e.into()) })?; - } - Ok(()) - })?; - } - Ok(()) + write_element_block("IDValue", w, |w| { + w.write(XmlEvent::Characters(&orcid)).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(()) + }) } } From f6a86a3ff0fb77e7df8757714feca42d93877ff0 Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Thu, 22 Jul 2021 15:45:51 +0100 Subject: [PATCH 08/14] Carry over minor corrections to existing logic back into Project Muse generation --- .../src/xml/onix3_project_muse.rs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/thoth-export-server/src/xml/onix3_project_muse.rs b/thoth-export-server/src/xml/onix3_project_muse.rs index 8ba0f31a..634dbbb6 100644 --- a/thoth-export-server/src/xml/onix3_project_muse.rs +++ b/thoth-export-server/src/xml/onix3_project_muse.rs @@ -294,9 +294,9 @@ impl XmlElementBlock for Work { w.write(XmlEvent::Characters("06")).map_err(|e| e.into()) })?; write_element_block("ProductIdentifier", w, |w| { - // 06 ISBN + // 15 ISBN-13 write_element_block("ProductIDType", w, |w| { - w.write(XmlEvent::Characters("06")).map_err(|e| e.into()) + 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()) @@ -308,15 +308,21 @@ impl XmlElementBlock for Work { })?; } write_element_block("ProductSupply", w, |w| { - let mut supplies: HashMap = HashMap::new(); + let mut supplies: HashMap = HashMap::new(); supplies.insert( pdf_url.to_string(), - "Publisher's website: download the title".to_string(), + ( + "29".to_string(), + "Publisher's website: download the title".to_string(), + ), ); if let Some(landing_page) = &self.landing_page { supplies.insert( landing_page.to_string(), - "Publisher's website: web shop".to_string(), + ( + "01".to_string(), + "Publisher's website: web shop".to_string(), + ), ); } for (url, description) in supplies.iter() { @@ -324,7 +330,7 @@ impl XmlElementBlock for Work { write_element_block("Supplier", w, |w| { // 09 Publisher to end-customers write_element_block("SupplierRole", w, |w| { - w.write(XmlEvent::Characters("11")).map_err(|e| e.into()) + w.write(XmlEvent::Characters("09")).map_err(|e| e.into()) })?; write_element_block("SupplierName", w, |w| { w.write(XmlEvent::Characters( @@ -335,10 +341,11 @@ impl XmlElementBlock for Work { write_element_block("Website", w, |w| { // 01 Publisher’s corporate website write_element_block("WebsiteRole", w, |w| { - w.write(XmlEvent::Characters("01")).map_err(|e| e.into()) + w.write(XmlEvent::Characters(&description.0)) + .map_err(|e| e.into()) })?; write_element_block("WebsiteDescription", w, |w| { - w.write(XmlEvent::Characters(&description)) + w.write(XmlEvent::Characters(&description.1)) .map_err(|e| e.into()) })?; write_element_block("WebsiteLink", w, |w| { From 266c8dc1b63b396cddc9c1be83b7f1ae7302c8c1 Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Thu, 22 Jul 2021 16:05:58 +0100 Subject: [PATCH 09/14] Add button for generating OAPEN ONIX directly from app --- thoth-app/src/models/work/mod.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/thoth-app/src/models/work/mod.rs b/thoth-app/src/models/work/mod.rs index c172e88b..0cf860b4 100644 --- a/thoth-app/src/models/work/mod.rs +++ b/thoth-app/src/models/work/mod.rs @@ -103,7 +103,8 @@ impl MetadataTable for WorkWithRelations { } pub trait DisplayWork { - fn onix_endpoint(&self) -> String; + fn onix_projectmuse_endpoint(&self) -> String; + fn onix_oapen_endpoint(&self) -> String; fn csv_endpoint(&self) -> String; fn cover_alt_text(&self) -> String; fn license_icons(&self) -> Html; @@ -112,13 +113,20 @@ pub trait DisplayWork { } impl DisplayWork for WorkWithRelations { - fn onix_endpoint(&self) -> String { + fn onix_projectmuse_endpoint(&self) -> String { format!( "{}/specifications/onix_3.0::project_muse/work/{}", THOTH_EXPORT_API, &self.work_id ) } + fn onix_oapen_endpoint(&self) -> String { + format!( + "{}/specifications/onix_3.0::oapen/work/{}", + THOTH_EXPORT_API, &self.work_id + ) + } + fn csv_endpoint(&self) -> String { format!( "{}/specifications/csv::thoth/work/{}", @@ -308,10 +316,16 @@ impl DisplayWork for WorkWithRelations { + - Date: Wed, 28 Jul 2021 11:46:21 +0200 Subject: [PATCH 13/14] Bump version v0.4.3 --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 12 ++++++------ thoth-api-server/Cargo.toml | 6 +++--- thoth-api/Cargo.toml | 4 ++-- thoth-app-server/Cargo.toml | 2 +- thoth-app/Cargo.toml | 6 +++--- thoth-app/manifest.json | 2 +- thoth-client/Cargo.toml | 6 +++--- thoth-errors/Cargo.toml | 2 +- thoth-export-server/Cargo.toml | 8 ++++---- 10 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 325f5e22..cca63b20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3936,7 +3936,7 @@ dependencies = [ [[package]] name = "thoth" -version = "0.4.2" +version = "0.4.3" dependencies = [ "cargo-husky", "clap", @@ -3951,7 +3951,7 @@ dependencies = [ [[package]] name = "thoth-api" -version = "0.4.2" +version = "0.4.3" dependencies = [ "actix-web", "argon2rs", @@ -3980,7 +3980,7 @@ dependencies = [ [[package]] name = "thoth-api-server" -version = "0.4.2" +version = "0.4.3" dependencies = [ "actix-cors", "actix-identity", @@ -3995,7 +3995,7 @@ dependencies = [ [[package]] name = "thoth-app" -version = "0.4.2" +version = "0.4.3" dependencies = [ "anyhow", "chrono", @@ -4018,7 +4018,7 @@ dependencies = [ [[package]] name = "thoth-app-server" -version = "0.4.2" +version = "0.4.3" dependencies = [ "actix-cors", "actix-web", @@ -4027,7 +4027,7 @@ dependencies = [ [[package]] name = "thoth-client" -version = "0.4.2" +version = "0.4.3" dependencies = [ "chrono", "graphql_client", @@ -4041,7 +4041,7 @@ dependencies = [ [[package]] name = "thoth-errors" -version = "0.4.2" +version = "0.4.3" dependencies = [ "actix-web", "csv", @@ -4056,7 +4056,7 @@ dependencies = [ [[package]] name = "thoth-export-server" -version = "0.4.2" +version = "0.4.3" dependencies = [ "actix-cors", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index 22e50036..18f53cee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth" -version = "0.4.2" +version = "0.4.3" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -16,11 +16,11 @@ maintenance = { status = "actively-developed" } members = ["thoth-api", "thoth-api-server", "thoth-app", "thoth-app-server", "thoth-client", "thoth-errors", "thoth-export-server"] [dependencies] -thoth-api = { version = "0.4.2", path = "thoth-api", features = ["backend"] } -thoth-api-server = { version = "0.4.2", path = "thoth-api-server" } -thoth-app-server = { version = "0.4.2", path = "thoth-app-server" } -thoth-errors = { version = "0.4.2", path = "thoth-errors" } -thoth-export-server = { version = "0.4.2", path = "thoth-export-server" } +thoth-api = { version = "0.4.3", path = "thoth-api", features = ["backend"] } +thoth-api-server = { version = "0.4.3", path = "thoth-api-server" } +thoth-app-server = { version = "0.4.3", path = "thoth-app-server" } +thoth-errors = { version = "0.4.3", path = "thoth-errors" } +thoth-export-server = { version = "0.4.3", path = "thoth-export-server" } clap = "2.33.3" dialoguer = "0.7.1" dotenv = "0.9.0" diff --git a/thoth-api-server/Cargo.toml b/thoth-api-server/Cargo.toml index 1b05925e..f542a490 100644 --- a/thoth-api-server/Cargo.toml +++ b/thoth-api-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-api-server" -version = "0.4.2" +version = "0.4.3" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -9,8 +9,8 @@ repository = "https://github.com/thoth-pub/thoth" readme = "README.md" [dependencies] -thoth-api = { version = "0.4.2", path = "../thoth-api", features = ["backend"] } -thoth-errors = { version = "0.4.2", path = "../thoth-errors" } +thoth-api = { version = "0.4.3", path = "../thoth-api", features = ["backend"] } +thoth-errors = { version = "0.4.3", path = "../thoth-errors" } actix-web = "3.3.2" actix-cors = "0.5.4" actix-identity = "0.3.1" diff --git a/thoth-api/Cargo.toml b/thoth-api/Cargo.toml index 8b73b4de..682cf003 100644 --- a/thoth-api/Cargo.toml +++ b/thoth-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-api" -version = "0.4.2" +version = "0.4.3" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -16,7 +16,7 @@ maintenance = { status = "actively-developed" } backend = ["diesel", "diesel-derive-enum", "diesel_migrations", "futures", "actix-web"] [dependencies] -thoth-errors = { version = "0.4.2", path = "../thoth-errors" } +thoth-errors = { version = "0.4.3", path = "../thoth-errors" } actix-web = { version = "3.3.2", optional = true } argon2rs = "0.2.5" isbn2 = "0.4.0" diff --git a/thoth-app-server/Cargo.toml b/thoth-app-server/Cargo.toml index 6d765517..8a4893cf 100644 --- a/thoth-app-server/Cargo.toml +++ b/thoth-app-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-app-server" -version = "0.4.2" +version = "0.4.3" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" diff --git a/thoth-app/Cargo.toml b/thoth-app/Cargo.toml index 3fbaf090..7488784a 100644 --- a/thoth-app/Cargo.toml +++ b/thoth-app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-app" -version = "0.4.2" +version = "0.4.3" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -33,5 +33,5 @@ serde = { version = "1.0.115", features = ["derive"] } serde_json = "1.0" url = "2.1.1" uuid = { version = "0.7", features = ["serde", "v4"] } -thoth-api = { version = "0.4.2", path = "../thoth-api" } -thoth-errors = { version = "0.4.2", path = "../thoth-errors" } +thoth-api = { version = "0.4.3", path = "../thoth-api" } +thoth-errors = { version = "0.4.3", path = "../thoth-errors" } diff --git a/thoth-app/manifest.json b/thoth-app/manifest.json index 2f6f7830..bc08a5f0 100644 --- a/thoth-app/manifest.json +++ b/thoth-app/manifest.json @@ -9,7 +9,7 @@ "start_url": "/?homescreen=1", "background_color": "#ffffff", "theme_color": "#ffdd57", - "version": "0.4.2", + "version": "0.4.3", "icons": [ { "src": "\/android-icon-36x36.png", diff --git a/thoth-client/Cargo.toml b/thoth-client/Cargo.toml index de8a68b3..687e4929 100644 --- a/thoth-client/Cargo.toml +++ b/thoth-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-client" -version = "0.4.2" +version = "0.4.3" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -9,8 +9,8 @@ repository = "https://github.com/thoth-pub/thoth" readme = "README.md" [dependencies] -thoth-api = {version = "0.4.2", path = "../thoth-api" } -thoth-errors = {version = "0.4.2", path = "../thoth-errors" } +thoth-api = {version = "0.4.3", path = "../thoth-api" } +thoth-errors = {version = "0.4.3", path = "../thoth-errors" } graphql_client = "0.9.0" chrono = { version = "0.4", features = ["serde"] } reqwest = { version = "0.10", features = ["json"] } diff --git a/thoth-errors/Cargo.toml b/thoth-errors/Cargo.toml index bbe32338..669f6dc9 100644 --- a/thoth-errors/Cargo.toml +++ b/thoth-errors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-errors" -version = "0.4.2" +version = "0.4.3" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" diff --git a/thoth-export-server/Cargo.toml b/thoth-export-server/Cargo.toml index 7f4bac29..8b9e66a2 100644 --- a/thoth-export-server/Cargo.toml +++ b/thoth-export-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-export-server" -version = "0.4.2" +version = "0.4.3" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -9,9 +9,9 @@ repository = "https://github.com/thoth-pub/thoth" readme = "README.md" [dependencies] -thoth-api = { version = "0.4.2", path = "../thoth-api" } -thoth-errors = { version = "0.4.2", path = "../thoth-errors" } -thoth-client = { version = "0.4.2", path = "../thoth-client" } +thoth-api = { version = "0.4.3", path = "../thoth-api" } +thoth-errors = { version = "0.4.3", path = "../thoth-errors" } +thoth-client = { version = "0.4.3", path = "../thoth-client" } actix-web = "3.3.2" actix-cors = "0.5.4" chrono = { version = "0.4", features = ["serde"] } From e3e82bfcbcda5532a94db877270eb6afef3f22fd Mon Sep 17 00:00:00 2001 From: Javier Arias Date: Wed, 28 Jul 2021 11:50:13 +0200 Subject: [PATCH 14/14] Update Changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8ce8489..1eb1a36f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to thoth will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.4.3]](https://github.com/thoth-pub/thoth/releases/tag/v0.4.3) - 2021-07-28 +### Added + - [#48](https://github.com/thoth-pub/thoth/issues/48) - Implement OAPEN ONIX 3.0 specification + +### Fixed + - [#254](https://github.com/thoth-pub/thoth/issues/254) - Ensure order of fields in create work match those in edit work + ## [[0.4.2]](https://github.com/thoth-pub/thoth/releases/tag/v0.4.2) - 2021-07-05 ### Added - [#125](https://github.com/thoth-pub/thoth/issues/125) - Implement `ISBN` type to standardise parsing