use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::data::{
Affiliation, Contributor, Data, Description, Identifier, License, Publisher, Subject, Title,
};
use crate::doi_utils::{normalize_doi, validate_doi};
use crate::error::{Error, Result};
use crate::utils::{
normalize_cc_url, normalize_id, normalize_orcid, normalize_ror, normalize_url, sanitize,
url_to_spdx, validate_id,
};
use crate::vocab::CONTRIBUTOR_ROLES;
#[derive(Deserialize, Default, Clone)]
pub(crate) struct SoOrganization {
#[serde(rename = "@id", default)]
pub(crate) id: String,
#[serde(rename = "sameAs", default)]
pub(crate) same_as: String,
#[serde(default)]
pub(crate) name: String,
}
#[derive(Deserialize, Default, Clone)]
pub(crate) struct SoContributor {
#[serde(rename = "@id", default)]
pub(crate) id: String,
#[serde(rename = "@type", default)]
pub(crate) type_: String,
#[serde(rename = "givenName", default)]
pub(crate) given_name: String,
#[serde(rename = "familyName", default)]
pub(crate) family_name: String,
#[serde(default)]
pub(crate) name: String,
#[serde(default)]
pub(crate) affiliation: Option<Value>,
}
#[allow(dead_code)]
#[derive(Deserialize, Default)]
struct SoCitation {
#[serde(rename = "@id", default)]
id: String,
#[serde(rename = "@type", default)]
type_: String,
#[serde(default)]
name: String,
}
#[allow(dead_code)]
#[derive(Deserialize, Default)]
struct SoPeriodical {
#[serde(rename = "@id", default)]
_id: String,
#[serde(rename = "@type", default)]
_type: String,
#[serde(default)]
name: String,
#[serde(default)]
issn: String,
}
#[derive(Deserialize, Default)]
struct SoPublisher {
#[serde(rename = "@type", default)]
_type: String,
#[serde(default)]
name: String,
}
#[derive(Deserialize, Default)]
struct SoContent {
#[serde(rename = "@context", default)]
_context: String,
#[serde(rename = "@id", default)]
id: String,
#[serde(rename = "@type", default)]
type_: String,
#[serde(rename = "additionalType", default)]
additional_type: String,
#[serde(default)]
author: Option<Value>,
#[serde(default)]
creator: Option<Value>,
#[serde(default)]
contributor: Option<Value>,
#[serde(default)]
editor: Option<Value>,
#[allow(dead_code)]
#[serde(default)]
citation: Vec<SoCitation>,
#[serde(rename = "dateCreated", default)]
date_created: String,
#[serde(rename = "datePublished", default)]
date_published: String,
#[serde(rename = "dateModified", default)]
date_modified: String,
#[serde(default)]
description: String,
#[serde(default)]
headline: String,
#[serde(default)]
identifier: Option<Value>,
#[serde(rename = "inLanguage", default)]
in_language: String,
#[serde(default)]
keywords: Option<Value>,
#[serde(default)]
license: String,
#[serde(default)]
name: String,
#[allow(dead_code)]
#[serde(default)]
periodical: Option<SoPeriodical>,
#[serde(default)]
publisher: Option<SoPublisher>,
#[serde(default)]
url: String,
#[serde(default)]
version: Option<Value>,
}
pub(crate) fn so_to_cm_type(so: &str) -> &'static str {
match so {
"Article" => "Article",
"BlogPosting" => "BlogPost",
"Book" => "Book",
"BookChapter" => "BookChapter",
"CreativeWork" => "Other",
"Dataset" => "Dataset",
"DigitalDocument" => "Document",
"Dissertation" => "Dissertation",
"Instrument" => "Instrument",
"NewsArticle" => "Article",
"Legislation" => "LegalDocument",
"Report" => "Report",
"ScholarlyArticle" => "JournalArticle",
"SoftwareSourceCode" => "Software",
_ => "",
}
}
pub(crate) fn value_to_contributors(v: &Value) -> Vec<SoContributor> {
if v.is_null() {
return vec![];
}
if let Ok(single) = serde_json::from_value::<SoContributor>(v.clone()) {
if !single.name.is_empty() || !single.given_name.is_empty() || !single.family_name.is_empty() {
return vec![single];
}
}
serde_json::from_value::<Vec<SoContributor>>(v.clone()).unwrap_or_default()
}
pub(crate) fn parse_affiliation(v: &Value) -> Option<Affiliation> {
if let Some(s) = v.as_str() {
if !s.is_empty() {
return Some(Affiliation { name: s.to_string(), ..Default::default() });
}
return None;
}
if let Ok(org) = serde_json::from_value::<SoOrganization>(v.clone()) {
if org.name.is_empty() {
return None;
}
let id = {
let ror = normalize_ror(&org.id);
if !ror.is_empty() {
ror
} else {
normalize_ror(&org.same_as)
}
};
return Some(Affiliation { id, name: org.name, ..Default::default() });
}
None
}
pub(crate) fn get_contributor(v: SoContributor, default_role: &str) -> Contributor {
let mut type_ = v.type_.clone();
let mut id = String::new();
if !v.id.is_empty() {
let orcid = normalize_orcid(&v.id);
if !orcid.is_empty() {
id = orcid;
type_ = "Person".to_string();
} else {
let ror = normalize_ror(&v.id);
if !ror.is_empty() {
id = ror;
type_ = "Organization".to_string();
}
}
}
let mut name = v.name.clone();
let mut given_name = v.given_name.clone();
let mut family_name = v.family_name.clone();
if type_.is_empty() {
if !given_name.is_empty() || !family_name.is_empty() {
type_ = "Person".to_string();
} else {
type_ = "Organization".to_string();
}
}
if type_ == "Person" && !name.is_empty() && given_name.is_empty() && family_name.is_empty() {
if let Some(comma) = name.find(',') {
family_name = name[..comma].trim().to_string();
given_name = name[comma + 1..].trim().to_string();
name = String::new();
} else {
let parts: Vec<&str> = name.splitn(2, ' ').collect();
if parts.len() == 2 {
given_name = parts[0].to_string();
family_name = parts[1].to_string();
name = String::new();
}
}
}
let affiliations: Vec<Affiliation> = if let Some(aff_val) = &v.affiliation {
if let Ok(list) = serde_json::from_value::<Vec<Value>>(aff_val.clone()) {
list.iter().filter_map(parse_affiliation).collect()
} else {
parse_affiliation(aff_val).into_iter().collect()
}
} else {
vec![]
};
let role = if CONTRIBUTOR_ROLES.contains(&v.type_.as_str()) {
v.type_.clone()
} else {
default_role.to_string()
};
Contributor {
id,
type_,
name,
given_name,
family_name,
affiliations,
contributor_roles: vec![role],
}
}
fn from_content(content: SoContent) -> Data {
let id = normalize_id(&content.id);
let cm_type = so_to_cm_type(&content.type_);
let type_ = if cm_type.is_empty() {
"WebPage".to_string()
} else {
cm_type.to_string()
};
let mut data = Data {
id,
type_,
additional_type: content.additional_type.clone(),
..Data::default()
};
let author_val = content.author.or(content.creator).unwrap_or(Value::Null);
for v in value_to_contributors(&author_val) {
if v.name.is_empty() && v.given_name.is_empty() && v.family_name.is_empty() {
continue;
}
let contrib = get_contributor(v, "Author");
let dup = !contrib.id.is_empty()
&& data.contributors.iter().any(|c| !c.id.is_empty() && c.id == contrib.id);
if !dup {
data.contributors.push(contrib);
}
}
if let Some(ref contrib_val) = content.contributor {
for v in value_to_contributors(contrib_val) {
if v.name.is_empty() && v.given_name.is_empty() && v.family_name.is_empty() {
continue;
}
let contrib = get_contributor(v, "Author");
let dup = !contrib.id.is_empty()
&& data.contributors.iter().any(|c| !c.id.is_empty() && c.id == contrib.id);
if !dup {
data.contributors.push(contrib);
}
}
}
if let Some(ref editor_val) = content.editor {
for v in value_to_contributors(editor_val) {
if v.name.is_empty() && v.given_name.is_empty() && v.family_name.is_empty() {
continue;
}
let contrib = get_contributor(v, "Editor");
let dup = !contrib.id.is_empty()
&& data.contributors.iter().any(|c| !c.id.is_empty() && c.id == contrib.id);
if !dup {
data.contributors.push(contrib);
}
}
}
if !content.date_published.is_empty() {
data.date.published = content.date_published.clone();
}
if !content.date_modified.is_empty() {
data.date.updated = content.date_modified.clone();
}
if !content.date_created.is_empty() {
data.date.created = content.date_created.clone();
}
if !content.description.is_empty() {
data.descriptions.push(Description {
description: sanitize(&content.description),
type_: "Abstract".to_string(),
language: String::new(),
});
}
let idents: Vec<String> = match &content.identifier {
Some(Value::String(s)) => vec![s.clone()],
Some(Value::Array(arr)) => arr
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect(),
_ => vec![],
};
for id_str in &idents {
if id_str != &data.id {
let (identifier, identifier_type) = validate_id(id_str);
if !identifier.is_empty() {
let identifier = if identifier_type == "DOI" {
normalize_doi(&identifier)
} else {
identifier
};
data.identifiers.push(Identifier { identifier, identifier_type: identifier_type.to_string() });
}
}
}
data.language = content.in_language.clone();
if !content.license.is_empty() {
let (url, ok) = normalize_cc_url(&content.license);
if ok {
let id = url_to_spdx(&url);
data.license = License { id, url };
}
}
if let Some(bare) = validate_doi(&data.id) {
use crate::doi_utils::get_doi_ra_sync;
if let Some(ra) = get_doi_ra_sync(&bare) {
data.provider = ra;
}
}
if let Some(pub_) = content.publisher
&& !pub_.name.is_empty() {
data.publisher = Publisher { name: pub_.name, ..Default::default() };
}
let keywords: Vec<String> = match &content.keywords {
Some(Value::String(s)) => s.split(',').map(|k| k.trim().to_string()).collect(),
Some(Value::Array(arr)) => arr
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect(),
_ => vec![],
};
for kw in keywords {
if !kw.is_empty() {
data.subjects.push(Subject { subject: kw });
}
}
if !content.name.is_empty() {
data.titles.push(Title { title: content.name.clone(), ..Default::default() });
} else if !content.headline.is_empty() {
data.titles.push(Title { title: content.headline.clone(), ..Default::default() });
}
if let Some(url) = normalize_url(&content.url, true, false) {
data.url = url;
}
if let Some(v) = &content.version {
data.version = match v {
Value::String(s) => s.clone(),
Value::Number(n) => format!("{}", n),
_ => String::new(),
};
}
data
}
pub fn read_json(input: &str) -> Result<Data> {
let content: SoContent =
serde_json::from_str(input).map_err(|e| Error::Parse(e.to_string()))?;
Ok(from_content(content))
}
pub fn fetch(url: &str) -> Result<Data> {
let client = reqwest::blocking::Client::builder()
.user_agent(format!(
"commonmeta-rs/{} (https://github.com/front-matter/commonmeta-rs; mailto:info@front-matter.de)",
env!("CARGO_PKG_VERSION")
))
.redirect(reqwest::redirect::Policy::limited(5))
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| Error::Http(e.to_string()))?;
let html = client
.get(url)
.send()
.map_err(|e| Error::Http(e.to_string()))?
.error_for_status()
.map_err(|e| Error::Http(e.to_string()))?
.text()
.map_err(|e| Error::Http(e.to_string()))?;
let content = extract_content(&html)?;
if let Some(bare) = validate_doi(&content.id) {
use crate::doi_utils::get_doi_ra_sync;
if let Some(ra) = get_doi_ra_sync(&bare) {
match ra.as_str() {
"Crossref" => return crate::formats::crossref::fetch(&content.id),
"DataCite" => return crate::formats::datacite::fetch(&content.id),
_ => {}
}
}
}
Ok(from_content(content))
}
fn extract_content(html: &str) -> Result<SoContent> {
use scraper::{Html, Selector};
let doc = Html::parse_document(html);
let script_sel =
Selector::parse("script[type='application/ld+json']").expect("valid selector");
let mut content = SoContent::default();
for el in doc.select(&script_sel) {
let text = el.text().collect::<String>();
if let Ok(parsed) = serde_json::from_str::<SoContent>(&text) {
content = parsed;
break;
}
}
if content.id.is_empty() {
let meta_names = [
"meta[name='citation_doi']",
"meta[name='dc.identifier']",
"meta[name='DC.identifier']",
"meta[name='bepress_citation_doi']",
];
for sel_str in &meta_names {
if let Ok(sel) = Selector::parse(sel_str)
&& let Some(el) = doc.select(&sel).next()
&& let Some(val) = el.value().attr("content") {
content.id = val.to_string();
break;
}
}
}
if content.type_.is_empty() {
let type_metas = [
"meta[property='og:type']",
"meta[name='dc.type']",
"meta[name='DC.type']",
];
for sel_str in &type_metas {
if let Ok(sel) = Selector::parse(sel_str)
&& let Some(el) = doc.select(&sel).next()
&& let Some(val) = el.value().attr("content") {
content.type_ = val.to_string();
break;
}
}
}
if content.name.is_empty() {
let name_metas = [
"meta[name='citation_title']",
"meta[name='dc.title']",
"meta[name='DC.title']",
"meta[property='og:title']",
"meta[name='twitter:title']",
];
for sel_str in &name_metas {
if let Ok(sel) = Selector::parse(sel_str)
&& let Some(el) = doc.select(&sel).next() {
let val = el
.value()
.attr("content")
.unwrap_or("")
.trim()
.to_string();
if !val.is_empty() {
content.name = val;
break;
}
}
}
}
if content.description.is_empty() {
let desc_metas = [
"meta[name='citation_abstract']",
"meta[name='dc.description']",
"meta[property='og:description']",
"meta[name='twitter:description']",
];
for sel_str in &desc_metas {
if let Ok(sel) = Selector::parse(sel_str)
&& let Some(el) = doc.select(&sel).next() {
let val = el
.value()
.attr("content")
.unwrap_or("")
.trim()
.to_string();
if !val.is_empty() {
content.description = val;
break;
}
}
}
}
if content.date_published.is_empty() {
let date_metas = [
"meta[name='citation_publication_date']",
"meta[name='citation_date']",
"meta[name='dc.date']",
"meta[property='article:published_time']",
];
for sel_str in &date_metas {
if let Ok(sel) = Selector::parse(sel_str)
&& let Some(el) = doc.select(&sel).next()
&& let Some(val) = el.value().attr("content") {
content.date_published = val.to_string();
break;
}
}
}
if content.date_modified.is_empty() {
let mod_metas = [
"meta[name='og:updated_time']",
"meta[name='article:modified_time']",
];
for sel_str in &mod_metas {
if let Ok(sel) = Selector::parse(sel_str)
&& let Some(el) = doc.select(&sel).next()
&& let Some(val) = el.value().attr("content") {
content.date_modified = val.to_string();
break;
}
}
}
if content.in_language.is_empty()
&& let Ok(sel) = Selector::parse("html")
&& let Some(el) = doc.select(&sel).next()
&& let Some(lang) = el.value().attr("lang") {
content.in_language = lang.to_string();
}
if content.license.is_empty()
&& let Ok(sel) = Selector::parse("link[rel='license']")
&& let Some(el) = doc.select(&sel).next()
&& let Some(href) = el.value().attr("href") {
content.license = href.to_string();
}
if content.author.is_none()
&& let Some(creator) = content.creator.take() {
content.author = Some(creator);
}
Ok(content)
}
#[derive(Serialize)]
struct OutOrganization {
#[serde(rename = "@id", skip_serializing_if = "String::is_empty")]
id: String,
#[serde(rename = "@type")]
type_: &'static str,
#[serde(skip_serializing_if = "String::is_empty")]
name: String,
}
#[derive(Serialize)]
struct OutContributor {
#[serde(rename = "@id", skip_serializing_if = "String::is_empty")]
id: String,
#[serde(rename = "@type")]
type_: &'static str,
#[serde(rename = "givenName", skip_serializing_if = "String::is_empty")]
given_name: String,
#[serde(rename = "familyName", skip_serializing_if = "String::is_empty")]
family_name: String,
#[serde(skip_serializing_if = "String::is_empty")]
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
affiliation: Option<OutOrganization>,
}
#[derive(Serialize)]
struct OutCitation {
#[serde(rename = "@id")]
id: String,
#[serde(rename = "@type")]
type_: String,
#[serde(skip_serializing_if = "String::is_empty")]
name: String,
}
#[derive(Serialize, Default)]
struct OutPeriodical {
#[serde(rename = "@id", skip_serializing_if = "String::is_empty")]
id: String,
#[serde(rename = "@type")]
type_: &'static str,
#[serde(skip_serializing_if = "String::is_empty")]
name: String,
#[serde(skip_serializing_if = "String::is_empty")]
issn: String,
}
#[derive(Serialize)]
struct OutDataCatalog {
#[serde(rename = "@id", skip_serializing_if = "String::is_empty")]
id: String,
#[serde(rename = "@type")]
type_: &'static str,
#[serde(skip_serializing_if = "String::is_empty")]
name: String,
}
#[derive(Serialize)]
struct OutMediaObject {
#[serde(rename = "@type")]
type_: &'static str,
#[serde(rename = "contentUrl")]
content_url: String,
#[serde(rename = "encodingFormat", skip_serializing_if = "String::is_empty")]
encoding_format: String,
#[serde(skip_serializing_if = "String::is_empty")]
name: String,
#[serde(skip_serializing_if = "String::is_empty")]
sha256: String,
#[serde(skip_serializing_if = "String::is_empty")]
size: String,
}
#[derive(Serialize)]
struct OutProvider {
#[serde(rename = "@type")]
type_: &'static str,
name: String,
}
#[derive(Serialize)]
struct OutPublisher {
#[serde(rename = "@type")]
type_: &'static str,
name: String,
}
#[derive(Serialize)]
struct OutPayload {
#[serde(rename = "@context")]
context: &'static str,
#[serde(rename = "@id")]
id: String,
#[serde(rename = "@type")]
type_: String,
#[serde(rename = "additionalType", skip_serializing_if = "String::is_empty")]
additional_type: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
author: Vec<OutContributor>,
#[serde(skip_serializing_if = "Vec::is_empty")]
editor: Vec<OutContributor>,
#[serde(skip_serializing_if = "Vec::is_empty")]
citation: Vec<OutCitation>,
#[serde(rename = "includedInDataCatalog", skip_serializing_if = "Option::is_none")]
included_in_data_catalog: Option<OutDataCatalog>,
#[serde(skip_serializing_if = "Option::is_none")]
periodical: Option<OutPeriodical>,
#[serde(rename = "dateCreated", skip_serializing_if = "String::is_empty")]
date_created: String,
#[serde(rename = "datePublished", skip_serializing_if = "String::is_empty")]
date_published: String,
#[serde(rename = "dateModified", skip_serializing_if = "String::is_empty")]
date_modified: String,
#[serde(skip_serializing_if = "String::is_empty")]
description: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
distribution: Vec<OutMediaObject>,
#[serde(skip_serializing_if = "Vec::is_empty")]
encoding: Vec<OutMediaObject>,
#[serde(skip_serializing_if = "Vec::is_empty")]
identifier: Vec<String>,
#[serde(rename = "inLanguage", skip_serializing_if = "String::is_empty")]
in_language: String,
#[serde(skip_serializing_if = "String::is_empty")]
keywords: String,
#[serde(skip_serializing_if = "String::is_empty")]
license: String,
#[serde(skip_serializing_if = "String::is_empty")]
name: String,
#[serde(rename = "pageStart", skip_serializing_if = "String::is_empty")]
page_start: String,
#[serde(rename = "pageEnd", skip_serializing_if = "String::is_empty")]
page_end: String,
provider: OutProvider,
publisher: OutPublisher,
#[serde(skip_serializing_if = "String::is_empty")]
url: String,
#[serde(skip_serializing_if = "String::is_empty")]
version: String,
}
fn cm_to_so_type(cm: &str) -> &'static str {
match cm {
"Article" => "Article",
"Audiovisual" => "CreativeWork",
"BlogPost" => "BlogPosting",
"Book" => "Book",
"BookChapter" => "BookChapter",
"Collection" => "CreativeWork",
"Dataset" => "Dataset",
"Dissertation" => "Dissertation",
"Document" => "CreativeWork",
"Entry" => "CreativeWork",
"Event" => "CreativeWork",
"Figure" => "CreativeWork",
"Image" => "CreativeWork",
"Instrument" => "Instrument",
"JournalArticle" => "ScholarlyArticle",
"LegalDocument" => "Legislation",
"Software" => "SoftwareSourceCode",
"Presentation" => "PresentationDigitalDocument",
_ => "CreativeWork",
}
}
fn convert(data: &crate::data::Data) -> OutPayload {
let mut authors: Vec<OutContributor> = Vec::new();
let mut editors: Vec<OutContributor> = Vec::new();
for c in &data.contributors {
let is_author = c.contributor_roles.contains(&"Author".to_string());
let is_editor = c.contributor_roles.contains(&"Editor".to_string());
if !is_author && !is_editor {
continue;
}
let affiliation = c.affiliations.first().map(|a| OutOrganization {
id: a.id.clone(),
type_: "Organization",
name: a.name.clone(),
});
let out = if c.type_ == "Organization" {
OutContributor {
id: c.id.clone(),
type_: "Organization",
given_name: String::new(),
family_name: String::new(),
name: c.name.clone(),
affiliation: None,
}
} else {
OutContributor {
id: c.id.clone(),
type_: "Person",
given_name: c.given_name.clone(),
family_name: c.family_name.clone(),
name: String::new(),
affiliation,
}
};
if is_author {
authors.push(out);
} else {
editors.push(out);
}
}
let citation: Vec<OutCitation> = data
.references
.iter()
.filter(|r| !r.id.is_empty())
.map(|r| {
let type_ = if r.type_ == "JournalArticle" {
"ScholarlyArticle".to_string()
} else {
"CreativeWork".to_string()
};
OutCitation { id: r.id.clone(), type_, name: r.title.clone() }
})
.collect();
let (included_in_data_catalog, periodical) = if data.type_ == "Dataset" {
let cat = OutDataCatalog {
id: data.container.identifier.clone(),
type_: "DataCatalog",
name: data.container.title.clone(),
};
(Some(cat), None)
} else {
let (id, issn) = if data.container.identifier_type == "ISSN" {
(String::new(), data.container.identifier.clone())
} else {
(data.container.identifier.clone(), String::new())
};
let p = OutPeriodical {
id,
type_: "Periodical",
name: data.container.title.clone(),
issn,
};
if p.name.is_empty() && p.issn.is_empty() && p.id.is_empty() {
(None, None)
} else {
(None, Some(p))
}
};
let media_objects: Vec<OutMediaObject> = data
.files
.iter()
.map(|f| OutMediaObject {
type_: "MediaObject",
content_url: f.url.clone(),
encoding_format: f.mime_type.clone(),
name: f.key.clone(),
sha256: f.checksum.clone(),
size: if f.size > 0 { f.size.to_string() } else { String::new() },
})
.collect();
let (distribution, encoding) = if data.type_ == "Dataset" {
(media_objects, vec![])
} else {
(vec![], media_objects)
};
let identifier: Vec<String> = data
.identifiers
.iter()
.map(|i| i.identifier.clone())
.collect();
let keywords = if data.subjects.is_empty() {
String::new()
} else {
data.subjects
.iter()
.filter(|s| !s.subject.is_empty())
.map(|s| s.subject.as_str())
.collect::<Vec<_>>()
.join(", ")
};
let name = data.titles.first().map(|t| t.title.clone()).unwrap_or_default();
let description = data
.descriptions
.first()
.map(|d| d.description.clone())
.unwrap_or_default();
OutPayload {
context: "http://schema.org",
id: data.id.clone(),
type_: cm_to_so_type(&data.type_).to_string(),
additional_type: data.additional_type.clone(),
author: authors,
editor: editors,
citation,
included_in_data_catalog,
periodical,
date_created: data.date.created.clone(),
date_published: data.date.published.clone(),
date_modified: data.date.updated.clone(),
description,
distribution,
encoding,
identifier,
in_language: data.language.clone(),
keywords,
license: data.license.url.clone(),
name,
page_start: data.container.first_page.clone(),
page_end: data.container.last_page.clone(),
provider: OutProvider { type_: "Organization", name: data.provider.clone() },
publisher: OutPublisher { type_: "Organization", name: data.publisher.name.clone() },
url: data.url.clone(),
version: data.version.clone(),
}
}
pub fn write(data: &crate::data::Data) -> crate::error::Result<Vec<u8>> {
let payload = convert(data);
serde_json::to_vec(&payload).map_err(|e| crate::error::Error::Serialize(e.to_string()))
}
pub fn write_all(list: &[crate::data::Data]) -> crate::error::Result<Vec<u8>> {
let payloads: Vec<OutPayload> = list.iter().map(convert).collect();
serde_json::to_vec_pretty(&payloads)
.map_err(|e| crate::error::Error::Serialize(e.to_string()))
}