1#[cfg(feature = "std")]
5use crate::error::ApiResult;
6#[cfg(feature = "std")]
7use crate::io::{read_file, write_file, InputOutput, License};
8#[cfg(feature = "std")]
9use crate::prelude::PathBuf;
10use crate::prelude::*;
11use crate::schema::validate::{
12 is_commit, is_country_code, is_date, is_doi, is_isbn, is_orcid, is_phone_number, is_semantic_version, is_states, IntegerOrString, MonthValue,
13 NumberOrString, PostalCode, YearValue,
14};
15#[cfg(not(feature = "std"))]
16use crate::util::License;
17#[cfg(feature = "std")]
18use crate::util::MimeType;
19use crate::util::{MarkdownSupport, ToProse};
20#[cfg(feature = "std")]
21use color_eyre::eyre::eyre;
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24use serde_with::skip_serializing_none;
25use validator::{Validate, ValidationErrors};
26
27pub type Catalog = Vec<Cff>;
29#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
31#[serde(untagged)]
32pub enum Agent {
33 Entity(Entity),
35 Person(Person),
37}
38#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
40#[serde(rename_all = "lowercase")]
41pub enum CffType {
42 Dataset,
44 Software,
46}
47#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
49#[serde(rename_all = "lowercase")]
50pub enum IdentifierType {
51 Doi,
53 Other,
55 Swh,
57 Url,
59}
60#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
62#[serde(rename_all = "kebab-case")]
63pub enum PublicationStatus {
64 Abstract,
66 AdvanceOnline,
68 InPreparation,
70 InPress,
72 Preprint,
74 Submitted,
76}
77#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
79#[serde(rename_all = "kebab-case")]
80pub enum ReferenceType {
81 Art,
83 Article,
85 Audiovisual,
87 Bill,
89 Blog,
91 Book,
93 Catalogue,
95 Conference,
97 ConferencePaper,
99 Data,
101 Database,
103 Dictionary,
105 EditedWork,
107 Encyclopedia,
109 FilmBroadcast,
111 Generic,
113 GovernmentDocument,
115 Grant,
117 Hearing,
119 HistoricalWork,
121 LegalCase,
123 LegalRule,
125 MagazineArticle,
127 Manual,
129 Map,
131 Multimedia,
133 Music,
135 NewspaperArticle,
137 Pamphlet,
139 Patent,
141 PersonalCommunication,
143 Proceedings,
145 Report,
147 Serial,
149 Slides,
151 Software,
153 SoftwareCode,
155 SoftwareContainer,
157 SoftwareExecutable,
159 SoftwareVirtualMachine,
161 SoundRecording,
163 Standard,
165 Statute,
167 Thesis,
169 Unpublished,
171 Video,
173 Website,
175}
176#[skip_serializing_none]
178#[derive(Clone, Debug, eserde::Deserialize, JsonSchema, PartialEq, Serialize, Validate)]
179#[serde(deny_unknown_fields, rename_all = "kebab-case")]
180pub struct Cff {
181 #[serde(rename = "abstract")]
183 pub abstract_text: Option<String>,
184 #[validate(nested)]
186 #[eserde(compat)]
187 pub authors: Vec<Agent>,
188 pub cff_version: String,
190 pub commit: Option<String>,
192 #[validate(nested)]
194 #[eserde(compat)]
195 pub contact: Option<Vec<Agent>>,
196 #[validate(custom(function = "is_date"))]
198 pub date_released: Option<String>,
199 #[validate(custom(function = "is_doi"))]
201 pub doi: Option<String>,
202 #[validate(nested)]
204 #[eserde(compat)]
205 pub identifiers: Option<Vec<Identifier>>,
206 pub keywords: Option<Vec<String>>,
208 #[validate(nested)]
210 #[eserde(compat)]
211 pub license: Option<License>,
212 #[validate(url)]
214 pub license_url: Option<String>,
215 pub message: String,
217 #[validate(nested)]
219 #[eserde(compat)]
220 pub preferred_citation: Option<Reference>,
221 #[validate(nested)]
223 #[eserde(compat)]
224 pub references: Option<Vec<Reference>>,
225 #[validate(url)]
227 pub repository: Option<String>,
228 #[validate(url)]
230 pub repository_artifact: Option<String>,
231 #[validate(url)]
233 pub repository_code: Option<String>,
234 pub title: String,
236 #[serde(rename = "type")]
238 #[eserde(compat)]
239 pub kind: Option<CffType>,
240 #[validate(url)]
242 pub url: Option<String>,
243 #[validate(custom(function = "is_semantic_version"))]
245 pub version: Option<String>,
246}
247impl Cff {
248 #[cfg(feature = "analysis")]
250 pub(crate) fn embedded(content: &str) -> Vec<Self> {
251 content
252 .split("```")
253 .enumerate()
254 .filter(|(index, _)| index % 2 == 1)
255 .filter_map(|(_, block)| {
256 let value = block
257 .strip_prefix("cff\n")
258 .or_else(|| block.strip_prefix("yaml\n"))
259 .or_else(|| block.strip_prefix("yml\n"))
260 .unwrap_or(block);
261 value
262 .contains("cff-version")
263 .then(|| serde_norway::from_str::<Self>(value).ok())
264 .flatten()
265 })
266 .collect()
267 }
268}
269#[skip_serializing_none]
271#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize, Validate)]
272#[serde(deny_unknown_fields, rename_all = "kebab-case")]
273pub struct Entity {
274 pub address: Option<String>,
276 pub alias: Option<String>,
278 pub city: Option<String>,
280 #[validate(custom(function = "is_country_code"))]
282 pub country: Option<String>,
283 #[validate(custom(function = "is_date"))]
285 pub date_end: Option<String>,
286 #[validate(custom(function = "is_date"))]
288 pub date_start: Option<String>,
289 #[validate(email)]
291 pub email: Option<String>,
292 pub fax: Option<String>,
294 pub location: Option<String>,
296 pub name: String,
298 #[validate(custom(function = "is_orcid"))]
300 pub orcid: Option<String>,
301 #[validate(nested)]
303 pub postal_code: Option<PostalCode>,
304 pub region: Option<String>,
306 #[validate(custom(function = "is_phone_number"))]
308 pub tel: Option<String>,
309 #[validate(url)]
311 pub website: Option<String>,
312}
313#[skip_serializing_none]
315#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize, Validate)]
316#[serde(deny_unknown_fields, rename_all = "kebab-case")]
317pub struct Identifier {
318 pub description: Option<String>,
320 #[serde(rename = "type")]
322 pub kind: IdentifierType,
323 pub value: String,
325}
326#[skip_serializing_none]
328#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize, Validate)]
329#[serde(deny_unknown_fields, rename_all = "kebab-case")]
330pub struct Person {
331 pub address: Option<String>,
333 pub affiliation: Option<String>,
335 pub alias: Option<String>,
337 pub city: Option<String>,
339 #[validate(custom(function = "is_country_code"))]
341 pub country: Option<String>,
342 #[validate(email)]
344 pub email: Option<String>,
345 pub family_names: Option<String>,
347 pub fax: Option<String>,
349 pub given_names: Option<String>,
351 pub name_particle: Option<String>,
353 pub name_suffix: Option<String>,
355 #[validate(custom(function = "is_orcid"))]
357 pub orcid: Option<String>,
358 #[validate(nested)]
360 pub postal_code: Option<PostalCode>,
361 pub region: Option<String>,
363 #[validate(custom(function = "is_phone_number"))]
365 pub tel: Option<String>,
366 #[validate(url)]
368 pub website: Option<String>,
369}
370#[skip_serializing_none]
372#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize, Validate)]
373#[serde(deny_unknown_fields, rename_all = "kebab-case")]
374pub struct Reference {
375 pub abbreviation: Option<String>,
377 #[serde(rename = "abstract")]
379 pub abstract_text: Option<String>,
380 #[validate(nested)]
382 pub authors: Vec<Agent>,
383 #[validate(custom(function = "is_doi"))]
385 pub collection_doi: Option<String>,
386 pub collection_title: Option<String>,
388 pub collection_type: Option<String>,
390 #[validate(custom(function = "is_commit"))]
392 pub commit: Option<String>,
393 #[validate(nested)]
395 pub conference: Option<Entity>,
396 #[validate(nested)]
398 pub contact: Option<Vec<Agent>>,
399 pub copyright: Option<String>,
401 pub data_type: Option<String>,
403 pub database: Option<String>,
405 pub database_provider: Option<Entity>,
407 #[validate(custom(function = "is_date"))]
409 pub date_accessed: Option<String>,
410 #[validate(custom(function = "is_date"))]
412 pub date_downloaded: Option<String>,
413 #[validate(custom(function = "is_date"))]
415 pub date_published: Option<String>,
416 #[validate(custom(function = "is_date"))]
418 pub date_released: Option<String>,
419 pub department: Option<String>,
422 #[validate(custom(function = "is_doi"))]
424 pub doi: Option<String>,
425 pub edition: Option<String>,
427 #[validate(nested)]
429 pub editors: Option<Vec<Agent>>,
430 #[validate(nested)]
432 pub editors_series: Option<Vec<Agent>>,
433 #[validate(nested)]
435 pub end: Option<IntegerOrString>,
436 pub entry: Option<String>,
438 pub filename: Option<String>,
440 pub format: Option<String>,
443 #[validate(nested)]
445 pub identifiers: Option<Vec<Identifier>>,
446 #[validate(nested)]
448 pub institution: Option<Entity>,
449 #[validate(custom(function = "is_isbn"))]
451 pub isbn: Option<String>,
452 pub issn: Option<String>,
455 pub issue: Option<NumberOrString>,
457 #[validate(custom(function = "is_date"))]
459 pub issue_date: Option<String>,
460 pub issue_title: Option<String>,
462 pub journal: Option<String>,
464 pub keywords: Option<Vec<String>>,
466 pub languages: Option<Vec<String>>,
468 #[validate(nested)]
470 pub license: Option<License>,
471 #[validate(url)]
473 pub license_url: Option<String>,
474 pub loc_end: Option<IntegerOrString>,
476 pub loc_start: Option<IntegerOrString>,
478 #[validate(nested)]
480 pub location: Option<Entity>,
481 pub medium: Option<String>,
483 #[validate(nested)]
485 pub month: Option<MonthValue>,
486 pub nihmsid: Option<String>,
491 pub notes: Option<String>,
493 pub number: Option<NumberOrString>,
495 pub number_volumes: Option<IntegerOrString>,
497 pub pages: Option<IntegerOrString>,
499 #[validate(custom(function = "is_states"))]
501 pub patent_states: Option<Vec<String>>,
502 pub pmcid: Option<String>,
504 #[validate(nested)]
506 pub publisher: Option<Entity>,
507 #[validate(nested)]
509 pub recipients: Option<Vec<Agent>>,
510 #[validate(url)]
512 pub repository: Option<String>,
513 #[validate(url)]
515 pub repository_artifact: Option<String>,
516 #[validate(url)]
518 pub repository_code: Option<String>,
519 pub scope: Option<String>,
523 #[validate(nested)]
525 pub section: Option<NumberOrString>,
526 #[validate(nested)]
528 pub senders: Option<Vec<Agent>>,
529 pub start: Option<IntegerOrString>,
531 pub status: Option<PublicationStatus>,
533 pub term: Option<String>,
535 pub thesis_type: Option<String>,
537 pub title: String,
539 #[validate(nested)]
541 pub translators: Option<Vec<Agent>>,
542 #[serde(rename = "type")]
544 pub kind: ReferenceType,
545 #[validate(url)]
547 pub url: Option<String>,
548 #[validate(custom(function = "is_semantic_version"))]
550 pub version: Option<String>,
551 pub volume: Option<IntegerOrString>,
553 pub volume_title: Option<String>,
555 #[validate(nested)]
557 pub year: Option<YearValue>,
558 #[validate(nested)]
560 pub year_original: Option<YearValue>,
561}
562impl Default for Cff {
563 fn default() -> Self {
564 Self {
565 abstract_text: None,
566 authors: Vec::new(),
567 cff_version: "1.2.0".to_string(),
568 commit: None,
569 contact: None,
570 date_released: None,
571 doi: None,
572 identifiers: None,
573 keywords: None,
574 license: None,
575 license_url: None,
576 message: "If you use this software, please cite it using the metadata provided in this file.".to_string(),
577 preferred_citation: None,
578 references: None,
579 repository: None,
580 repository_artifact: None,
581 repository_code: None,
582 title: String::new(),
583 kind: None,
584 url: None,
585 version: None,
586 }
587 }
588}
589impl MarkdownSupport for Cff {
590 fn to_markdown(&self) -> String {
591 serde_norway::to_string(self).unwrap_or_default()
592 }
593}
594impl ToProse for Cff {
595 fn to_prose(&self) -> String {
596 [Some(self.title.to_string()), self.abstract_text.clone(), Some(self.message.clone())]
597 .into_iter()
598 .flatten()
599 .collect::<Vec<String>>()
600 .join("\n\n")
601 }
602}
603#[cfg(feature = "std")]
604impl InputOutput for Cff {
605 fn read(path: impl Into<PathBuf>) -> ApiResult<Cff> {
606 let source = path.into();
607 match MimeType::from(source.display().to_string()) {
608 | MimeType::Cff | MimeType::Yaml => Cff::read_yaml(source),
609 | MimeType::Json => Cff::read_json(source),
610 | _ => Err(eyre!("Unsupported CFF data file extension")),
611 }
612 }
613 fn read_cff(path: impl Into<PathBuf>) -> ApiResult<Cff> {
614 Cff::read_yaml(path.into())
615 }
616 fn read_json(path: PathBuf) -> ApiResult<Cff> {
617 read_file(path.clone()).and_then(|content| {
618 eserde::json::from_str::<Cff>(&content).map_err(|errors| {
619 let details: Vec<String> = errors
620 .iter()
621 .map(|e| format!("{}: {}", e.path().map_or("root".into(), |p| p.to_string()), e.message()))
622 .collect();
623 eyre!("{}", details.join("\n"))
624 })
625 })
626 }
627 fn read_yaml(path: PathBuf) -> ApiResult<Cff> {
628 read_file(path.clone()).and_then(|content| serde_norway::from_str(&content).map_err(|why| eyre!("Failed to parse YAML CFF — {why}")))
629 }
630 fn write(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
631 let output = path.into();
632 match MimeType::from(output.display().to_string()) {
633 | MimeType::Cff => self.write_cff(output),
634 | MimeType::Json => self.write_json(output),
635 | MimeType::Yaml => self.write_yaml(output),
636 | _ => Err(eyre!("Unsupported CFF data file extension for writing")),
637 }
638 }
639 fn write_cff(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
640 let output = path.into().with_extension("cff");
641 serde_norway::to_string(self)
642 .map_err(|why| eyre!("Failed to serialize CFF — {why}"))
643 .and_then(|content| write_file(output, content))
644 }
645 fn write_json(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
646 let output = path.into().with_extension("json");
647 serde_json::to_string_pretty(self)
648 .map_err(|why| eyre!("Failed to serialize JSON CFF — {why}"))
649 .and_then(|content| write_file(output, content))
650 }
651 fn write_yaml(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
652 let output = path.into().with_extension("yaml");
653 serde_norway::to_string(self)
654 .map_err(|why| eyre!("Failed to serialize YAML CFF — {why}"))
655 .and_then(|content| write_file(output, content))
656 }
657}
658impl Validate for Agent {
659 fn validate(&self) -> Result<(), ValidationErrors> {
660 match self {
661 | Self::Entity(value) => value.validate(),
662 | Self::Person(value) => value.validate(),
663 }
664 }
665}