#[cfg(feature = "std")]
use crate::io::{read_file, write_file, ApiResult, InputOutput, License};
#[cfg(feature = "std")]
use crate::prelude::PathBuf;
use crate::prelude::*;
use crate::schema::validate::{
is_commit, is_country_code, is_date, is_doi, is_isbn, is_orcid, is_phone_number, is_semantic_version, is_states, IntegerOrString, MonthValue,
NumberOrString, PostalCode, YearValue,
};
#[cfg(not(feature = "std"))]
use crate::util::License;
#[cfg(feature = "std")]
use crate::util::MimeType;
use crate::util::{ToMarkdown, ToProse};
#[cfg(feature = "std")]
use color_eyre::eyre::eyre;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use validator::{Validate, ValidationErrors};
pub type Catalog = Vec<Cff>;
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
#[serde(untagged)]
pub enum Agent {
Entity(Entity),
Person(Person),
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum CffType {
Dataset,
Software,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum IdentifierType {
Doi,
Other,
Swh,
Url,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum PublicationStatus {
Abstract,
AdvanceOnline,
InPreparation,
InPress,
Preprint,
Submitted,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ReferenceType {
Art,
Article,
Audiovisual,
Bill,
Blog,
Book,
Catalogue,
Conference,
ConferencePaper,
Data,
Database,
Dictionary,
EditedWork,
Encyclopedia,
FilmBroadcast,
Generic,
GovernmentDocument,
Grant,
Hearing,
HistoricalWork,
LegalCase,
LegalRule,
MagazineArticle,
Manual,
Map,
Multimedia,
Music,
NewspaperArticle,
Pamphlet,
Patent,
PersonalCommunication,
Proceedings,
Report,
Serial,
Slides,
Software,
SoftwareCode,
SoftwareContainer,
SoftwareExecutable,
SoftwareVirtualMachine,
SoundRecording,
Standard,
Statute,
Thesis,
Unpublished,
Video,
Website,
}
#[skip_serializing_none]
#[derive(Clone, Debug, eserde::Deserialize, JsonSchema, PartialEq, Serialize, Validate)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Cff {
#[serde(rename = "abstract")]
pub abstract_text: Option<String>,
#[validate(nested)]
#[eserde(compat)]
pub authors: Vec<Agent>,
pub cff_version: String,
pub commit: Option<String>,
#[validate(nested)]
#[eserde(compat)]
pub contact: Option<Vec<Agent>>,
#[validate(custom(function = "is_date"))]
pub date_released: Option<String>,
#[validate(custom(function = "is_doi"))]
pub doi: Option<String>,
#[validate(nested)]
#[eserde(compat)]
pub identifiers: Option<Vec<Identifier>>,
pub keywords: Option<Vec<String>>,
#[validate(nested)]
#[eserde(compat)]
pub license: Option<License>,
#[validate(url)]
pub license_url: Option<String>,
pub message: String,
#[validate(nested)]
#[eserde(compat)]
pub preferred_citation: Option<Reference>,
#[validate(nested)]
#[eserde(compat)]
pub references: Option<Vec<Reference>>,
#[validate(url)]
pub repository: Option<String>,
#[validate(url)]
pub repository_artifact: Option<String>,
#[validate(url)]
pub repository_code: Option<String>,
pub title: String,
#[serde(rename = "type")]
#[eserde(compat)]
pub kind: Option<CffType>,
#[validate(url)]
pub url: Option<String>,
#[validate(custom(function = "is_semantic_version"))]
pub version: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize, Validate)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Entity {
pub address: Option<String>,
pub alias: Option<String>,
pub city: Option<String>,
#[validate(custom(function = "is_country_code"))]
pub country: Option<String>,
#[validate(custom(function = "is_date"))]
pub date_end: Option<String>,
#[validate(custom(function = "is_date"))]
pub date_start: Option<String>,
#[validate(email)]
pub email: Option<String>,
pub fax: Option<String>,
pub location: Option<String>,
pub name: String,
#[validate(custom(function = "is_orcid"))]
pub orcid: Option<String>,
#[validate(nested)]
pub postal_code: Option<PostalCode>,
pub region: Option<String>,
#[validate(custom(function = "is_phone_number"))]
pub tel: Option<String>,
#[validate(url)]
pub website: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize, Validate)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Identifier {
pub description: Option<String>,
#[serde(rename = "type")]
pub kind: IdentifierType,
pub value: String,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize, Validate)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Person {
pub address: Option<String>,
pub affiliation: Option<String>,
pub alias: Option<String>,
pub city: Option<String>,
#[validate(custom(function = "is_country_code"))]
pub country: Option<String>,
#[validate(email)]
pub email: Option<String>,
pub family_names: Option<String>,
pub fax: Option<String>,
pub given_names: Option<String>,
pub name_particle: Option<String>,
pub name_suffix: Option<String>,
#[validate(custom(function = "is_orcid"))]
pub orcid: Option<String>,
#[validate(nested)]
pub postal_code: Option<PostalCode>,
pub region: Option<String>,
#[validate(custom(function = "is_phone_number"))]
pub tel: Option<String>,
#[validate(url)]
pub website: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize, Validate)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Reference {
pub abbreviation: Option<String>,
#[serde(rename = "abstract")]
pub abstract_text: Option<String>,
#[validate(nested)]
pub authors: Vec<Agent>,
#[validate(custom(function = "is_doi"))]
pub collection_doi: Option<String>,
pub collection_title: Option<String>,
pub collection_type: Option<String>,
#[validate(custom(function = "is_commit"))]
pub commit: Option<String>,
#[validate(nested)]
pub conference: Option<Entity>,
#[validate(nested)]
pub contact: Option<Vec<Agent>>,
pub copyright: Option<String>,
pub data_type: Option<String>,
pub database: Option<String>,
pub database_provider: Option<Entity>,
#[validate(custom(function = "is_date"))]
pub date_accessed: Option<String>,
#[validate(custom(function = "is_date"))]
pub date_downloaded: Option<String>,
#[validate(custom(function = "is_date"))]
pub date_published: Option<String>,
#[validate(custom(function = "is_date"))]
pub date_released: Option<String>,
pub department: Option<String>,
#[validate(custom(function = "is_doi"))]
pub doi: Option<String>,
pub edition: Option<String>,
#[validate(nested)]
pub editors: Option<Vec<Agent>>,
#[validate(nested)]
pub editors_series: Option<Vec<Agent>>,
#[validate(nested)]
pub end: Option<IntegerOrString>,
pub entry: Option<String>,
pub filename: Option<String>,
pub format: Option<String>,
#[validate(nested)]
pub identifiers: Option<Vec<Identifier>>,
#[validate(nested)]
pub institution: Option<Entity>,
#[validate(custom(function = "is_isbn"))]
pub isbn: Option<String>,
pub issn: Option<String>,
pub issue: Option<NumberOrString>,
#[validate(custom(function = "is_date"))]
pub issue_date: Option<String>,
pub issue_title: Option<String>,
pub journal: Option<String>,
pub keywords: Option<Vec<String>>,
pub languages: Option<Vec<String>>,
#[validate(nested)]
pub license: Option<License>,
#[validate(url)]
pub license_url: Option<String>,
pub loc_end: Option<IntegerOrString>,
pub loc_start: Option<IntegerOrString>,
#[validate(nested)]
pub location: Option<Entity>,
pub medium: Option<String>,
#[validate(nested)]
pub month: Option<MonthValue>,
pub nihmsid: Option<String>,
pub notes: Option<String>,
pub number: Option<NumberOrString>,
pub number_volumes: Option<IntegerOrString>,
pub pages: Option<IntegerOrString>,
#[validate(custom(function = "is_states"))]
pub patent_states: Option<Vec<String>>,
pub pmcid: Option<String>,
#[validate(nested)]
pub publisher: Option<Entity>,
#[validate(nested)]
pub recipients: Option<Vec<Agent>>,
#[validate(url)]
pub repository: Option<String>,
#[validate(url)]
pub repository_artifact: Option<String>,
#[validate(url)]
pub repository_code: Option<String>,
pub scope: Option<String>,
#[validate(nested)]
pub section: Option<NumberOrString>,
#[validate(nested)]
pub senders: Option<Vec<Agent>>,
pub start: Option<IntegerOrString>,
pub status: Option<PublicationStatus>,
pub term: Option<String>,
pub thesis_type: Option<String>,
pub title: String,
#[validate(nested)]
pub translators: Option<Vec<Agent>>,
#[serde(rename = "type")]
pub kind: ReferenceType,
#[validate(url)]
pub url: Option<String>,
#[validate(custom(function = "is_semantic_version"))]
pub version: Option<String>,
pub volume: Option<IntegerOrString>,
pub volume_title: Option<String>,
#[validate(nested)]
pub year: Option<YearValue>,
#[validate(nested)]
pub year_original: Option<YearValue>,
}
impl Default for Cff {
fn default() -> Self {
Self {
abstract_text: None,
authors: Vec::new(),
cff_version: "1.2.0".to_string(),
commit: None,
contact: None,
date_released: None,
doi: None,
identifiers: None,
keywords: None,
license: None,
license_url: None,
message: "If you use this software, please cite it using the metadata provided in this file.".to_string(),
preferred_citation: None,
references: None,
repository: None,
repository_artifact: None,
repository_code: None,
title: String::new(),
kind: None,
url: None,
version: None,
}
}
}
impl ToMarkdown for Cff {
fn to_markdown(&self) -> String {
serde_norway::to_string(self).unwrap_or_default()
}
}
impl ToProse for Cff {
fn to_prose(&self) -> String {
[Some(self.title.to_string()), self.abstract_text.clone(), Some(self.message.clone())]
.into_iter()
.flatten()
.collect::<Vec<String>>()
.join("\n\n")
}
}
#[cfg(feature = "std")]
impl InputOutput for Cff {
fn read(path: impl Into<PathBuf>) -> ApiResult<Cff> {
let source = path.into();
match MimeType::from(source.display().to_string()) {
| MimeType::Cff | MimeType::Yaml => Cff::read_yaml(source),
| MimeType::Json => Cff::read_json(source),
| _ => Err(eyre!("Unsupported CFF data file extension")),
}
}
fn read_cff(path: impl Into<PathBuf>) -> ApiResult<Cff> {
Cff::read_yaml(path.into())
}
fn read_json(path: PathBuf) -> ApiResult<Cff> {
read_file(path.clone()).and_then(|content| {
eserde::json::from_str::<Cff>(&content).map_err(|errors| {
let details: Vec<String> = errors
.iter()
.map(|e| format!("{}: {}", e.path().map_or("root".into(), |p| p.to_string()), e.message()))
.collect();
eyre!("{}", details.join("\n"))
})
})
}
fn read_yaml(path: PathBuf) -> ApiResult<Cff> {
read_file(path.clone()).and_then(|content| serde_norway::from_str(&content).map_err(|why| eyre!("Failed to parse YAML CFF — {why}")))
}
fn write(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
let output = path.into();
match MimeType::from(output.display().to_string()) {
| MimeType::Cff => self.write_cff(output),
| MimeType::Json => self.write_json(output),
| MimeType::Yaml => self.write_yaml(output),
| _ => Err(eyre!("Unsupported CFF data file extension for writing")),
}
}
fn write_cff(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
let output = path.into().with_extension("cff");
serde_norway::to_string(self)
.map_err(|why| eyre!("Failed to serialize CFF — {why}"))
.and_then(|content| write_file(output, content))
}
fn write_json(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
let output = path.into().with_extension("json");
serde_json::to_string_pretty(self)
.map_err(|why| eyre!("Failed to serialize JSON CFF — {why}"))
.and_then(|content| write_file(output, content))
}
fn write_yaml(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
let output = path.into().with_extension("yaml");
serde_norway::to_string(self)
.map_err(|why| eyre!("Failed to serialize YAML CFF — {why}"))
.and_then(|content| write_file(output, content))
}
}
impl Validate for Agent {
fn validate(&self) -> Result<(), ValidationErrors> {
match self {
| Self::Entity(value) => value.validate(),
| Self::Person(value) => value.validate(),
}
}
}