use super::common::{
ArchiveInfo, EprintInfo, FieldLanguageMap, HasNumbering, LangID, NormalizeNumbering, NumOrStr,
Numbering, Publisher, RefID, RichText, Title,
};
use crate::reference::WorkRelation;
use crate::reference::contributor::{
Contributor, ContributorEntry, ContributorList, ContributorRole,
};
use crate::reference::date::EdtfString;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cfg(feature = "bindings")]
use specta::Type;
use std::collections::HashMap;
use url::Url;
fn fold_contributors(
mut contributors: Vec<ContributorEntry>,
shorthands: &[(ContributorRole, Option<&Contributor>)],
) -> Vec<ContributorEntry> {
for (role, maybe_c) in shorthands {
if let Some(c) = maybe_c
&& !contributors
.iter()
.any(|e| &e.role == role && &e.contributor == *c)
{
contributors.push(ContributorEntry {
role: role.clone(),
contributor: (*c).clone(),
gender: None,
});
}
}
contributors
}
fn collect_contributors_by_role(
entries: &[ContributorEntry],
role: &ContributorRole,
) -> Option<Contributor> {
let matching: Vec<&Contributor> = entries
.iter()
.filter(|entry| &entry.role == role)
.map(|entry| &entry.contributor)
.collect();
match matching.as_slice() {
[] => None,
[single] => Some((*single).clone()),
_ => Some(Contributor::ContributorList(ContributorList(
matching.into_iter().cloned().collect(),
))),
}
}
struct ContributorViews {
author: Option<Contributor>,
editor: Option<Contributor>,
translator: Option<Contributor>,
}
fn reconcile_contributors(
contributors: Vec<ContributorEntry>,
author: Option<Contributor>,
editor: Option<Contributor>,
translator: Option<Contributor>,
) -> (Vec<ContributorEntry>, ContributorViews) {
let folded = fold_contributors(
contributors,
&[
(ContributorRole::Author, author.as_ref()),
(ContributorRole::Editor, editor.as_ref()),
(ContributorRole::Translator, translator.as_ref()),
],
);
let views = ContributorViews {
author: collect_contributors_by_role(&folded, &ContributorRole::Author),
editor: collect_contributors_by_role(&folded, &ContributorRole::Editor),
translator: collect_contributors_by_role(&folded, &ContributorRole::Translator),
};
(folded, views)
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(from = "MonographDeser", rename_all = "kebab-case")]
pub struct Monograph {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<RefID>,
pub r#type: MonographType,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<Title>,
#[serde(skip_serializing_if = "Option::is_none")]
pub short_title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub container: Option<WorkRelation>,
#[serde(skip_serializing)]
pub author: Option<Contributor>,
#[serde(skip_serializing)]
pub editor: Option<Contributor>,
#[serde(skip_serializing)]
pub translator: Option<Contributor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contributors: Vec<ContributorEntry>,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default, skip_serializing_if = "EdtfString::is_empty")]
pub created: EdtfString,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default, skip_serializing_if = "EdtfString::is_empty")]
pub issued: EdtfString,
#[serde(skip_serializing_if = "Option::is_none")]
pub publisher: Option<Publisher>,
#[serde(alias = "URL", skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
#[serde(skip_serializing_if = "Option::is_none")]
pub accessed: Option<EdtfString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<LangID>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub field_languages: FieldLanguageMap,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<RichText>,
#[serde(skip_serializing_if = "Option::is_none")]
pub abstract_text: Option<RichText>,
#[serde(alias = "ISBN", skip_serializing_if = "Option::is_none")]
pub isbn: Option<String>,
#[serde(alias = "DOI", skip_serializing_if = "Option::is_none")]
pub doi: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ads_bibcode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub volume: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issue: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub part_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supplement_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub printing_number: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub numbering: Vec<Numbering>,
#[serde(skip_serializing_if = "Option::is_none")]
pub genre: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub medium: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub archive: Option<String>,
#[serde(alias = "archive_location", skip_serializing_if = "Option::is_none")]
pub archive_location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub archive_info: Option<ArchiveInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub eprint: Option<EprintInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keywords: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub original: Option<WorkRelation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
#[serde(skip_serializing_if = "Option::is_none")]
pub available_date: Option<EdtfString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub references: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scale: Option<String>,
#[serde(
flatten,
default,
skip_serializing_if = "std::collections::BTreeMap::is_empty"
)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub unknown_fields: std::collections::BTreeMap<String, serde_json::Value>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
struct MonographDeser {
id: Option<RefID>,
r#type: MonographType,
title: Option<Title>,
short_title: Option<String>,
container: Option<WorkRelation>,
author: Option<Contributor>,
editor: Option<Contributor>,
translator: Option<Contributor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
contributors: Vec<ContributorEntry>,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default)]
created: EdtfString,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default)]
issued: EdtfString,
publisher: Option<Publisher>,
#[serde(alias = "URL")]
url: Option<Url>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
accessed: Option<EdtfString>,
language: Option<LangID>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
field_languages: FieldLanguageMap,
note: Option<RichText>,
abstract_text: Option<RichText>,
#[serde(alias = "ISBN")]
isbn: Option<String>,
#[serde(alias = "DOI")]
doi: Option<String>,
ads_bibcode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
volume: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
issue: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
edition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
part_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
supplement_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
printing_number: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
numbering: Vec<Numbering>,
genre: Option<String>,
medium: Option<String>,
archive: Option<String>,
#[serde(alias = "archive_location")]
archive_location: Option<String>,
archive_info: Option<ArchiveInfo>,
eprint: Option<EprintInfo>,
keywords: Option<Vec<String>>,
original: Option<WorkRelation>,
status: Option<String>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
available_date: Option<EdtfString>,
size: Option<String>,
duration: Option<String>,
references: Option<String>,
scale: Option<String>,
#[serde(flatten, default)]
unknown_fields: std::collections::BTreeMap<String, serde_json::Value>,
}
impl From<MonographDeser> for Monograph {
fn from(raw: MonographDeser) -> Self {
let (contributors, views) =
reconcile_contributors(raw.contributors, raw.author, raw.editor, raw.translator);
let mut monograph = Self {
id: raw.id,
r#type: raw.r#type,
title: raw.title,
short_title: raw.short_title,
container: raw.container,
author: views.author,
editor: views.editor,
translator: views.translator,
contributors,
created: raw.created,
issued: raw.issued,
publisher: raw.publisher,
url: raw.url,
accessed: raw.accessed,
language: raw.language,
field_languages: raw.field_languages,
note: raw.note,
abstract_text: raw.abstract_text,
isbn: raw.isbn,
doi: raw.doi,
ads_bibcode: raw.ads_bibcode,
volume: raw.volume,
issue: raw.issue,
edition: raw.edition,
number: raw.number,
part_number: raw.part_number,
supplement_number: raw.supplement_number,
printing_number: raw.printing_number,
numbering: raw.numbering,
genre: raw.genre,
medium: raw.medium,
archive: raw.archive,
archive_location: raw.archive_location,
archive_info: raw.archive_info,
eprint: raw.eprint,
keywords: raw.keywords,
original: raw.original,
status: raw.status,
available_date: raw.available_date,
size: raw.size,
duration: raw.duration,
references: raw.references,
scale: raw.scale,
unknown_fields: raw.unknown_fields,
};
monograph.normalize_numbering();
monograph
}
}
crate::tolerant_enum! {
#[derive(Debug, Default, Clone, PartialEq)]
pub enum MonographType {
#[default]
Book = "book",
Manual = "manual",
Report = "report",
Thesis = "thesis",
Webpage = "webpage",
Post = "post",
Interview = "interview",
Manuscript = "manuscript",
Preprint = "preprint",
PersonalCommunication = "personal-communication",
Document = "document"
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(from = "CollectionDeser", rename_all = "kebab-case")]
pub struct Collection {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<RefID>,
pub r#type: CollectionType,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<Title>,
#[serde(skip_serializing_if = "Option::is_none")]
pub short_title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub container: Option<WorkRelation>,
#[serde(skip_serializing)]
pub editor: Option<Contributor>,
#[serde(skip_serializing)]
pub translator: Option<Contributor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contributors: Vec<ContributorEntry>,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default, skip_serializing_if = "EdtfString::is_empty")]
pub created: EdtfString,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default, skip_serializing_if = "EdtfString::is_empty")]
pub issued: EdtfString,
#[serde(skip_serializing_if = "Option::is_none")]
pub publisher: Option<Publisher>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub volume: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issue: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub part_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supplement_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub printing_number: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub numbering: Vec<Numbering>,
#[serde(alias = "URL", skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
#[serde(skip_serializing_if = "Option::is_none")]
pub accessed: Option<EdtfString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<LangID>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub field_languages: FieldLanguageMap,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<RichText>,
#[serde(alias = "ISBN", skip_serializing_if = "Option::is_none")]
pub isbn: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event: Option<WorkRelation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keywords: Option<Vec<String>>,
#[serde(
flatten,
default,
skip_serializing_if = "std::collections::BTreeMap::is_empty"
)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub unknown_fields: std::collections::BTreeMap<String, serde_json::Value>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
struct CollectionDeser {
id: Option<RefID>,
r#type: CollectionType,
title: Option<Title>,
short_title: Option<String>,
container: Option<WorkRelation>,
editor: Option<Contributor>,
translator: Option<Contributor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
contributors: Vec<ContributorEntry>,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default)]
created: EdtfString,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default)]
issued: EdtfString,
publisher: Option<Publisher>,
#[serde(default, skip_serializing_if = "Option::is_none")]
volume: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
issue: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
edition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
part_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
supplement_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
printing_number: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
numbering: Vec<Numbering>,
#[serde(alias = "URL")]
url: Option<Url>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
accessed: Option<EdtfString>,
language: Option<LangID>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
field_languages: FieldLanguageMap,
note: Option<RichText>,
#[serde(alias = "ISBN")]
isbn: Option<String>,
event: Option<WorkRelation>,
keywords: Option<Vec<String>>,
#[serde(flatten, default)]
unknown_fields: std::collections::BTreeMap<String, serde_json::Value>,
}
impl From<CollectionDeser> for Collection {
fn from(raw: CollectionDeser) -> Self {
let (contributors, views) =
reconcile_contributors(raw.contributors, None, raw.editor, raw.translator);
let mut collection = Self {
id: raw.id,
r#type: raw.r#type,
title: raw.title,
short_title: raw.short_title,
container: raw.container,
editor: views.editor,
translator: views.translator,
contributors,
created: raw.created,
issued: raw.issued,
publisher: raw.publisher,
volume: raw.volume,
issue: raw.issue,
edition: raw.edition,
number: raw.number,
part_number: raw.part_number,
supplement_number: raw.supplement_number,
printing_number: raw.printing_number,
numbering: raw.numbering,
url: raw.url,
accessed: raw.accessed,
language: raw.language,
field_languages: raw.field_languages,
note: raw.note,
isbn: raw.isbn,
event: raw.event,
keywords: raw.keywords,
unknown_fields: raw.unknown_fields,
};
collection.normalize_numbering();
collection
}
}
crate::tolerant_enum! {
#[derive(Debug, Default, Clone, PartialEq)]
pub enum CollectionType {
#[default]
Anthology = "anthology",
Proceedings = "proceedings",
EditedBook = "edited-book",
EditedVolume = "edited-volume"
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(from = "CollectionComponentDeser", rename_all = "kebab-case")]
pub struct CollectionComponent {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<RefID>,
pub r#type: MonographComponentType,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<Title>,
#[serde(skip_serializing)]
pub author: Option<Contributor>,
#[serde(skip_serializing)]
pub translator: Option<Contributor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contributors: Vec<ContributorEntry>,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default, skip_serializing_if = "EdtfString::is_empty")]
pub created: EdtfString,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default, skip_serializing_if = "EdtfString::is_empty")]
pub issued: EdtfString,
#[serde(skip_serializing_if = "Option::is_none")]
pub container: Option<WorkRelation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub volume: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issue: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub part_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supplement_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub printing_number: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub numbering: Vec<Numbering>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pages: Option<NumOrStr>,
#[serde(alias = "URL", skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
#[serde(skip_serializing_if = "Option::is_none")]
pub accessed: Option<EdtfString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<LangID>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub field_languages: FieldLanguageMap,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<RichText>,
#[serde(skip_serializing_if = "Option::is_none")]
pub abstract_text: Option<RichText>,
#[serde(alias = "DOI", skip_serializing_if = "Option::is_none")]
pub doi: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub genre: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub medium: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub archive_info: Option<ArchiveInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub eprint: Option<EprintInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keywords: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub original: Option<WorkRelation>,
#[serde(
flatten,
default,
skip_serializing_if = "std::collections::BTreeMap::is_empty"
)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub unknown_fields: std::collections::BTreeMap<String, serde_json::Value>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
struct CollectionComponentDeser {
id: Option<RefID>,
r#type: MonographComponentType,
title: Option<Title>,
author: Option<Contributor>,
translator: Option<Contributor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
contributors: Vec<ContributorEntry>,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default)]
created: EdtfString,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default)]
issued: EdtfString,
container: Option<WorkRelation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
volume: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
issue: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
edition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
part_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
supplement_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
printing_number: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
numbering: Vec<Numbering>,
pages: Option<NumOrStr>,
#[serde(alias = "URL")]
url: Option<Url>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
accessed: Option<EdtfString>,
language: Option<LangID>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
field_languages: FieldLanguageMap,
note: Option<RichText>,
abstract_text: Option<RichText>,
#[serde(alias = "DOI")]
doi: Option<String>,
genre: Option<String>,
medium: Option<String>,
status: Option<String>,
archive_info: Option<ArchiveInfo>,
eprint: Option<EprintInfo>,
keywords: Option<Vec<String>>,
original: Option<WorkRelation>,
#[serde(flatten, default)]
unknown_fields: std::collections::BTreeMap<String, serde_json::Value>,
}
impl From<CollectionComponentDeser> for CollectionComponent {
fn from(raw: CollectionComponentDeser) -> Self {
let (contributors, views) =
reconcile_contributors(raw.contributors, raw.author, None, raw.translator);
let mut component = Self {
id: raw.id,
r#type: raw.r#type,
title: raw.title,
author: views.author,
translator: views.translator,
contributors,
created: raw.created,
issued: raw.issued,
container: raw.container,
volume: raw.volume,
issue: raw.issue,
edition: raw.edition,
number: raw.number,
part_number: raw.part_number,
supplement_number: raw.supplement_number,
printing_number: raw.printing_number,
numbering: raw.numbering,
pages: raw.pages,
url: raw.url,
accessed: raw.accessed,
language: raw.language,
field_languages: raw.field_languages,
note: raw.note,
abstract_text: raw.abstract_text,
doi: raw.doi,
genre: raw.genre,
medium: raw.medium,
status: raw.status,
archive_info: raw.archive_info,
eprint: raw.eprint,
keywords: raw.keywords,
original: raw.original,
unknown_fields: raw.unknown_fields,
};
component.normalize_numbering();
component
}
}
crate::tolerant_enum! {
#[derive(Debug, Default, Clone, PartialEq)]
pub enum MonographComponentType {
#[default]
Chapter = "chapter",
Document = "document"
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(from = "SerialComponentDeser", rename_all = "kebab-case")]
pub struct SerialComponent {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<RefID>,
pub r#type: SerialComponentType,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<Title>,
#[serde(skip_serializing)]
pub author: Option<Contributor>,
#[serde(skip_serializing)]
pub translator: Option<Contributor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contributors: Vec<ContributorEntry>,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default, skip_serializing_if = "EdtfString::is_empty")]
pub created: EdtfString,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default, skip_serializing_if = "EdtfString::is_empty")]
pub issued: EdtfString,
#[serde(skip_serializing_if = "Option::is_none")]
pub container: Option<WorkRelation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub volume: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issue: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub part_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supplement_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub printing_number: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub numbering: Vec<Numbering>,
#[serde(alias = "URL", skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
#[serde(skip_serializing_if = "Option::is_none")]
pub accessed: Option<EdtfString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<LangID>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub field_languages: FieldLanguageMap,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<RichText>,
#[serde(skip_serializing_if = "Option::is_none")]
pub abstract_text: Option<RichText>,
#[serde(alias = "DOI", skip_serializing_if = "Option::is_none")]
pub doi: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ads_bibcode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pages: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub genre: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub medium: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub archive_info: Option<ArchiveInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub eprint: Option<EprintInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keywords: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub section: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
#[serde(skip_serializing_if = "Option::is_none")]
pub available_date: Option<EdtfString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reviewed: Option<WorkRelation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub original: Option<WorkRelation>,
#[serde(
flatten,
default,
skip_serializing_if = "std::collections::BTreeMap::is_empty"
)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub unknown_fields: std::collections::BTreeMap<String, serde_json::Value>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
struct SerialComponentDeser {
id: Option<RefID>,
r#type: SerialComponentType,
title: Option<Title>,
author: Option<Contributor>,
translator: Option<Contributor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
contributors: Vec<ContributorEntry>,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default)]
created: EdtfString,
#[cfg_attr(feature = "bindings", specta(type = String))]
#[serde(default)]
issued: EdtfString,
container: Option<WorkRelation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
volume: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
issue: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
edition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
part_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
supplement_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
printing_number: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
numbering: Vec<Numbering>,
#[serde(alias = "URL")]
url: Option<Url>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
accessed: Option<EdtfString>,
language: Option<LangID>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
field_languages: FieldLanguageMap,
note: Option<RichText>,
abstract_text: Option<RichText>,
#[serde(alias = "DOI")]
doi: Option<String>,
ads_bibcode: Option<String>,
pages: Option<String>,
genre: Option<String>,
medium: Option<String>,
archive_info: Option<ArchiveInfo>,
eprint: Option<EprintInfo>,
keywords: Option<Vec<String>>,
section: Option<String>,
status: Option<String>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
available_date: Option<EdtfString>,
reviewed: Option<WorkRelation>,
original: Option<WorkRelation>,
#[serde(flatten, default)]
unknown_fields: std::collections::BTreeMap<String, serde_json::Value>,
}
impl From<SerialComponentDeser> for SerialComponent {
fn from(raw: SerialComponentDeser) -> Self {
let (contributors, views) =
reconcile_contributors(raw.contributors, raw.author, None, raw.translator);
let mut component = Self {
id: raw.id,
r#type: raw.r#type,
title: raw.title,
author: views.author,
translator: views.translator,
contributors,
created: raw.created,
issued: raw.issued,
container: raw.container,
volume: raw.volume,
issue: raw.issue,
edition: raw.edition,
number: raw.number,
part_number: raw.part_number,
supplement_number: raw.supplement_number,
printing_number: raw.printing_number,
numbering: raw.numbering,
url: raw.url,
accessed: raw.accessed,
language: raw.language,
field_languages: raw.field_languages,
note: raw.note,
abstract_text: raw.abstract_text,
doi: raw.doi,
ads_bibcode: raw.ads_bibcode,
pages: raw.pages,
genre: raw.genre,
medium: raw.medium,
archive_info: raw.archive_info,
eprint: raw.eprint,
keywords: raw.keywords,
section: raw.section,
status: raw.status,
available_date: raw.available_date,
reviewed: raw.reviewed,
original: raw.original,
unknown_fields: raw.unknown_fields,
};
component.normalize_numbering();
component
}
}
crate::tolerant_enum! {
#[derive(Debug, Default, Clone, PartialEq)]
pub enum SerialComponentType {
#[default]
Article = "article",
Post = "post",
Review = "review"
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(from = "SerialDeser", rename_all = "kebab-case")]
pub struct Serial {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<RefID>,
pub r#type: SerialType,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<Title>,
#[serde(skip_serializing_if = "Option::is_none")]
pub short_title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub container: Option<WorkRelation>,
#[serde(skip_serializing)]
pub editor: Option<Contributor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contributors: Vec<ContributorEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub publisher: Option<Publisher>,
#[serde(alias = "URL", skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
#[serde(skip_serializing_if = "Option::is_none")]
pub accessed: Option<EdtfString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<LangID>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub field_languages: FieldLanguageMap,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<RichText>,
#[serde(alias = "ISSN", skip_serializing_if = "Option::is_none")]
pub issn: Option<String>,
#[serde(
flatten,
default,
skip_serializing_if = "std::collections::BTreeMap::is_empty"
)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub unknown_fields: std::collections::BTreeMap<String, serde_json::Value>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
struct SerialDeser {
id: Option<RefID>,
r#type: SerialType,
title: Option<Title>,
short_title: Option<String>,
container: Option<WorkRelation>,
editor: Option<Contributor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
contributors: Vec<ContributorEntry>,
publisher: Option<Publisher>,
#[serde(alias = "URL")]
url: Option<Url>,
#[cfg_attr(feature = "bindings", specta(type = Option<String>))]
accessed: Option<EdtfString>,
language: Option<LangID>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
field_languages: FieldLanguageMap,
note: Option<RichText>,
#[serde(alias = "ISSN")]
issn: Option<String>,
#[serde(flatten, default)]
unknown_fields: std::collections::BTreeMap<String, serde_json::Value>,
}
impl From<SerialDeser> for Serial {
fn from(raw: SerialDeser) -> Self {
let (contributors, views) =
reconcile_contributors(raw.contributors, None, raw.editor, None);
Self {
id: raw.id,
r#type: raw.r#type,
title: raw.title,
short_title: raw.short_title,
container: raw.container,
editor: views.editor,
contributors,
publisher: raw.publisher,
url: raw.url,
accessed: raw.accessed,
language: raw.language,
field_languages: raw.field_languages,
note: raw.note,
issn: raw.issn,
unknown_fields: raw.unknown_fields,
}
}
}
impl HasNumbering for Monograph {
fn numbering(&self) -> &[Numbering] {
&self.numbering
}
}
impl NormalizeNumbering for Monograph {
fn numbering_mut(&mut self) -> &mut Vec<Numbering> {
&mut self.numbering
}
fn volume_mut(&mut self) -> &mut Option<String> {
&mut self.volume
}
fn issue_mut(&mut self) -> &mut Option<String> {
&mut self.issue
}
fn edition_mut(&mut self) -> &mut Option<String> {
&mut self.edition
}
fn number_mut(&mut self) -> &mut Option<String> {
&mut self.number
}
fn part_number_mut(&mut self) -> &mut Option<String> {
&mut self.part_number
}
fn supplement_number_mut(&mut self) -> &mut Option<String> {
&mut self.supplement_number
}
fn printing_number_mut(&mut self) -> &mut Option<String> {
&mut self.printing_number
}
}
impl HasNumbering for Collection {
fn numbering(&self) -> &[Numbering] {
&self.numbering
}
}
impl NormalizeNumbering for Collection {
fn numbering_mut(&mut self) -> &mut Vec<Numbering> {
&mut self.numbering
}
fn volume_mut(&mut self) -> &mut Option<String> {
&mut self.volume
}
fn issue_mut(&mut self) -> &mut Option<String> {
&mut self.issue
}
fn edition_mut(&mut self) -> &mut Option<String> {
&mut self.edition
}
fn number_mut(&mut self) -> &mut Option<String> {
&mut self.number
}
fn part_number_mut(&mut self) -> &mut Option<String> {
&mut self.part_number
}
fn supplement_number_mut(&mut self) -> &mut Option<String> {
&mut self.supplement_number
}
fn printing_number_mut(&mut self) -> &mut Option<String> {
&mut self.printing_number
}
}
impl HasNumbering for CollectionComponent {
fn numbering(&self) -> &[Numbering] {
&self.numbering
}
}
impl NormalizeNumbering for CollectionComponent {
fn numbering_mut(&mut self) -> &mut Vec<Numbering> {
&mut self.numbering
}
fn volume_mut(&mut self) -> &mut Option<String> {
&mut self.volume
}
fn issue_mut(&mut self) -> &mut Option<String> {
&mut self.issue
}
fn edition_mut(&mut self) -> &mut Option<String> {
&mut self.edition
}
fn number_mut(&mut self) -> &mut Option<String> {
&mut self.number
}
fn part_number_mut(&mut self) -> &mut Option<String> {
&mut self.part_number
}
fn supplement_number_mut(&mut self) -> &mut Option<String> {
&mut self.supplement_number
}
fn printing_number_mut(&mut self) -> &mut Option<String> {
&mut self.printing_number
}
}
impl HasNumbering for SerialComponent {
fn numbering(&self) -> &[Numbering] {
&self.numbering
}
}
impl NormalizeNumbering for SerialComponent {
fn numbering_mut(&mut self) -> &mut Vec<Numbering> {
&mut self.numbering
}
fn volume_mut(&mut self) -> &mut Option<String> {
&mut self.volume
}
fn issue_mut(&mut self) -> &mut Option<String> {
&mut self.issue
}
fn edition_mut(&mut self) -> &mut Option<String> {
&mut self.edition
}
fn number_mut(&mut self) -> &mut Option<String> {
&mut self.number
}
fn part_number_mut(&mut self) -> &mut Option<String> {
&mut self.part_number
}
fn supplement_number_mut(&mut self) -> &mut Option<String> {
&mut self.supplement_number
}
fn printing_number_mut(&mut self) -> &mut Option<String> {
&mut self.printing_number
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum SerialType {
#[default]
AcademicJournal,
Blog,
Magazine,
Newspaper,
Newsletter,
Proceedings,
Podcast,
BroadcastProgram,
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
use super::*;
use crate::reference::types::common::NumberingType;
#[test]
fn monograph_part_number_normalizes_to_numbering() {
let json = r#"{
"type": "book",
"title": "Test Book",
"part-number": "II"
}"#;
let monograph: Monograph =
serde_json::from_str(json).expect("monograph should deserialize");
assert!(monograph.part_number.is_none());
assert!(
monograph
.numbering
.iter()
.any(|n| n.r#type == NumberingType::Part && n.value == "II")
);
}
#[test]
fn serial_component_supplement_number_normalizes_to_numbering() {
let json = r#"{
"type": "article",
"title": "Test Article",
"supplement-number": "S1"
}"#;
let component: SerialComponent =
serde_json::from_str(json).expect("serial component should deserialize");
assert!(component.supplement_number.is_none());
assert!(
component
.numbering
.iter()
.any(|n| n.r#type == NumberingType::Supplement && n.value == "S1")
);
}
#[test]
fn monograph_printing_number_normalizes_to_numbering() {
let json = r#"{
"type": "book",
"title": "Test Book",
"printing-number": "3rd printing"
}"#;
let monograph: Monograph =
serde_json::from_str(json).expect("monograph should deserialize");
assert!(monograph.printing_number.is_none());
assert!(
monograph
.numbering
.iter()
.any(|n| n.r#type == NumberingType::Printing && n.value == "3rd printing")
);
}
#[test]
fn collection_component_printing_number_normalizes_to_numbering() {
let json = r#"{
"type": "chapter",
"title": "Test Chapter",
"printing-number": "2"
}"#;
let component: CollectionComponent =
serde_json::from_str(json).expect("collection component should deserialize");
assert!(component.printing_number.is_none());
assert!(
component
.numbering
.iter()
.any(|n| n.r#type == NumberingType::Printing && n.value == "2")
);
}
}