1#[cfg(feature = "std")]
15use crate::error::ApiResult;
16#[cfg(feature = "std")]
17use crate::io::{read_file, write_file, InputOutput};
18use crate::prelude::*;
19use crate::schema::research_activity::s3a::Status;
20use crate::schema::standard::crosswalk::{self, mapping::datacite_to_invenio, CrosswalkError, FieldValue, Fields, SchemaBuilder, SchemaExtractor};
21use crate::schema::standard::datacite;
22use crate::schema::validate::{is_doi, is_iso_639_language_code};
23#[cfg(feature = "std")]
24use crate::util::MimeType;
25use crate::util::ToProse;
26#[cfg(feature = "std")]
27use crate::PathBuf;
28#[cfg(feature = "std")]
29use color_eyre::eyre::eyre;
30use core::fmt;
31use schemars::JsonSchema;
32use serde::{Deserialize, Serialize};
33use serde_json::Value;
34use serde_with::skip_serializing_none;
35use validator::Validate;
36
37pub type Catalog = Vec<Record>;
39#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
41#[serde(rename_all = "lowercase")]
42pub enum AccessLevel {
43 Public,
45 Restricted,
47}
48#[skip_serializing_none]
53#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
54pub struct Access {
55 pub record: Option<AccessLevel>,
57 pub files: Option<AccessLevel>,
59 #[validate(nested)]
61 pub embargo: Option<Embargo>,
62}
63#[skip_serializing_none]
65#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
66pub struct AdditionalDescription {
67 pub description: String,
69 #[serde(rename = "type")]
71 pub kind: Option<DescriptionType>,
72 #[validate(nested)]
74 pub lang: Option<LanguageValue>,
75}
76#[skip_serializing_none]
78#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
79pub struct AdditionalTitle {
80 pub title: String,
82 #[serde(rename = "type")]
84 pub kind: Option<TitleType>,
85 #[validate(nested)]
87 pub lang: Option<LanguageValue>,
88}
89#[skip_serializing_none]
91#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
92pub struct Affiliation {
93 pub id: Option<String>,
95 pub name: Option<String>,
97}
98#[skip_serializing_none]
100#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
101pub struct Award {
102 pub id: Option<String>,
104 pub title: Option<Value>,
106 pub number: Option<String>,
108 #[validate(nested)]
110 pub identifiers: Option<Vec<Identifier>>,
111}
112#[skip_serializing_none]
114#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
115pub struct CodeMeta {
116 #[validate(url)]
118 pub code_repository: Option<String>,
119 pub programming_language: Option<String>,
122 pub development_status: Option<Status>,
124}
125#[skip_serializing_none]
129#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
130pub struct Contributor {
131 #[validate(nested)]
133 pub person_or_org: PersonOrOrganization,
134 #[validate(nested)]
136 pub role: ControlledVocabularyValue,
137 #[validate(nested)]
139 pub affiliations: Option<Vec<Affiliation>>,
140}
141#[skip_serializing_none]
143#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
144pub struct ControlledVocabularyValue {
145 pub id: String,
147}
148#[skip_serializing_none]
152#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
153pub struct Creator {
154 #[validate(nested)]
156 pub person_or_org: PersonOrOrganization,
157 #[validate(nested)]
159 pub role: Option<ControlledVocabularyValue>,
160 #[validate(nested)]
162 pub affiliations: Option<Vec<Affiliation>>,
163}
164#[skip_serializing_none]
169#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
170pub struct CustomFields {
171 pub journal: Option<Journal>,
173 pub imprint: Option<Imprint>,
175 pub thesis: Option<Thesis>,
177 pub meeting: Option<Meeting>,
179 pub codemeta: Option<CodeMeta>,
181}
182#[skip_serializing_none]
184#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
185pub struct Date {
186 pub date: String,
188 #[serde(rename = "type")]
190 pub kind: DateType,
191 pub description: Option<String>,
193}
194#[skip_serializing_none]
196#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
197pub struct DateType {
198 pub id: Option<String>,
200 pub title: Option<Value>,
202}
203#[skip_serializing_none]
205#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
206pub struct DescriptionType {
207 pub id: Option<String>,
209 pub title: Option<Value>,
211}
212#[skip_serializing_none]
216#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
217pub struct Embargo {
218 pub active: bool,
220 pub until: Option<String>,
222 pub reason: Option<String>,
224}
225#[skip_serializing_none]
227#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate, PartialEq, Eq)]
228pub struct ExternalPersistentIdentifier {
229 #[validate(custom(function = "is_doi"))]
231 pub identifier: String,
232 pub provider: String,
234 pub client: Option<String>,
236}
237#[skip_serializing_none]
241#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
242pub struct ExternalPersistentIdentifiers {
243 #[validate(nested)]
245 pub doi: Option<ExternalPersistentIdentifier>,
246 #[serde(rename = "concept-doi")]
248 #[validate(nested)]
249 pub concept_doi: Option<ExternalPersistentIdentifier>,
250 #[validate(nested)]
252 pub handle: Option<ExternalPersistentIdentifier>,
253 #[validate(nested)]
255 pub oai: Option<ExternalPersistentIdentifier>,
256}
257#[skip_serializing_none]
259#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
260pub struct Files {
261 pub enabled: bool,
263 pub entries: Option<Value>,
265 pub default_preview: Option<String>,
267}
268#[skip_serializing_none]
270#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
271pub struct Funder {
272 pub id: Option<String>,
274 pub name: Option<String>,
276}
277#[skip_serializing_none]
279#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
280pub struct FundingReference {
281 #[validate(nested)]
283 pub funder: Funder,
284 pub award: Option<Award>,
286}
287#[skip_serializing_none]
289#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
290pub struct Identifier {
291 pub identifier: String,
293 pub scheme: String,
295}
296#[skip_serializing_none]
298#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
299pub struct Imprint {
300 pub title: Option<String>,
302 pub place: Option<String>,
304 pub pages: Option<String>,
306 pub isbn: Option<String>,
308 pub edition: Option<String>,
310}
311#[skip_serializing_none]
313#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
314pub struct Journal {
315 pub title: Option<String>,
317 pub issn: Option<String>,
319 pub volume: Option<String>,
321 pub issue: Option<String>,
323 pub pages: Option<String>,
325}
326#[skip_serializing_none]
328#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
329pub struct Language {
330 #[validate(custom(function = "is_iso_639_language_code"))]
332 pub id: String,
333}
334#[skip_serializing_none]
336#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
337pub struct LanguageValue {
338 #[validate(custom(function = "is_iso_639_language_code"))]
340 pub id: String,
341}
342#[skip_serializing_none]
344#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
345pub struct LocationFeature {
346 pub geometry: Option<Value>,
348 pub identifiers: Option<Vec<LocationIdentifier>>,
350 pub place: Option<String>,
352 pub description: Option<String>,
354}
355#[skip_serializing_none]
357#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
358pub struct LocationIdentifier {
359 pub identifier: String,
361 pub scheme: String,
363}
364#[skip_serializing_none]
366#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
367pub struct Locations {
368 pub features: Option<Vec<LocationFeature>>,
370}
371#[skip_serializing_none]
373#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
374pub struct Meeting {
375 pub title: Option<String>,
377 pub acronym: Option<String>,
379 pub dates: Option<String>,
381 pub place: Option<String>,
383 pub session: Option<String>,
385 pub session_part: Option<String>,
387 #[serde(rename = "url")]
389 pub url: Option<String>,
390 #[validate(nested)]
392 pub identifiers: Option<Vec<Identifier>>,
393}
394#[skip_serializing_none]
398#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
399pub struct Metadata {
400 #[validate(required, nested)]
402 pub resource_type: Option<ResourceType>,
403 #[validate(required)]
405 pub title: Option<String>,
406 #[validate(required)]
408 pub publication_date: Option<String>,
409 #[validate(required, nested)]
411 pub creators: Option<Vec<Creator>>,
412 pub additional_titles: Option<Vec<AdditionalTitle>>,
414 pub description: Option<String>,
416 pub additional_descriptions: Option<Vec<AdditionalDescription>>,
418 pub rights: Option<Vec<Right>>,
420 pub copyright: Option<String>,
422 #[validate(nested)]
424 pub contributors: Option<Vec<Contributor>>,
425 #[validate(nested)]
427 pub subjects: Option<Vec<Subject>>,
428 #[validate(nested)]
430 pub languages: Option<Vec<Language>>,
431 #[validate(nested)]
433 pub dates: Option<Vec<Date>>,
434 pub version: Option<String>,
436 pub publisher: Option<String>,
438 #[validate(nested)]
440 pub identifiers: Option<Vec<Identifier>>,
441 pub related_identifiers: Option<Vec<RelatedIdentifier>>,
443 pub sizes: Option<Vec<String>>,
445 pub formats: Option<Vec<String>>,
449 pub locations: Option<Locations>,
451 #[validate(nested)]
453 pub funding: Option<Vec<FundingReference>>,
454 #[validate(nested)]
456 pub references: Option<Vec<Reference>>,
457}
458#[skip_serializing_none]
460#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
461pub struct Owner {
462 pub user: Option<i32>,
464}
465#[skip_serializing_none]
470#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
471pub struct Parent {
472 pub id: String,
474 #[validate(nested)]
476 pub access: Option<ParentAccess>,
477}
478#[skip_serializing_none]
480#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
481pub struct ParentAccess {
482 #[validate(nested)]
484 pub owned_by: Option<Owner>,
485}
486#[skip_serializing_none]
491#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate, PartialEq, Eq)]
492pub struct PersistentIdentifier {
493 pub pk: Option<i32>,
495 pub status: Option<String>,
497}
498#[skip_serializing_none]
500#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
501pub struct PersonOrOrganization {
502 #[serde(rename = "type")]
504 pub kind: String,
505 pub name: Option<String>,
507 pub given_name: Option<String>,
509 pub family_name: Option<String>,
511 #[validate(nested)]
513 pub identifiers: Option<Vec<PersonOrOrgIdentifier>>,
514}
515#[skip_serializing_none]
517#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
518pub struct PersonOrOrgIdentifier {
520 pub scheme: String,
523 pub identifier: String,
525}
526#[skip_serializing_none]
531#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
532#[serde(deny_unknown_fields)]
533pub struct Record {
534 #[serde(rename = "$schema")]
536 pub schema: Option<String>,
537 pub id: Option<String>,
539 #[validate(nested)]
541 pub pid: Option<PersistentIdentifier>,
542 #[validate(nested)]
544 pub pids: Option<ExternalPersistentIdentifiers>,
545 #[validate(nested)]
547 pub parent: Option<Parent>,
548 #[validate(nested)]
550 pub access: Option<Access>,
551 #[validate(nested)]
553 pub metadata: Option<Metadata>,
554 pub custom_fields: Option<CustomFields>,
556 #[validate(nested)]
558 pub files: Option<Files>,
559 #[validate(nested)]
561 pub tombstone: Option<Tombstone>,
562 pub created: Option<String>,
564 pub updated: Option<String>,
566}
567#[skip_serializing_none]
569#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
570pub struct Reference {
571 pub reference: String,
573 pub scheme: Option<String>,
575 pub identifier: Option<String>,
577}
578#[skip_serializing_none]
580#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
581pub struct RelatedIdentifier {
582 pub identifier: String,
584 pub scheme: String,
586 pub relation_type: Option<RelationType>,
588 pub resource_type: Option<ResourceTypeInfo>,
590}
591#[skip_serializing_none]
593#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
594pub struct RelationType {
595 pub id: Option<String>,
597 pub title: Option<Value>,
599}
600#[skip_serializing_none]
602#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
603pub struct ResourceType {
604 pub id: Option<String>,
606 pub title: Option<Value>,
608}
609#[skip_serializing_none]
611#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
612pub struct ResourceTypeInfo {
613 pub id: Option<String>,
615 pub title: Option<Value>,
617}
618#[skip_serializing_none]
620#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
621pub struct Right {
622 pub id: Option<String>,
624 pub title: Option<Value>,
626 pub description: Option<Value>,
628 pub link: Option<String>,
630}
631#[skip_serializing_none]
633#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
634pub struct Subject {
635 pub id: Option<String>,
637 pub subject: Option<String>,
639}
640#[skip_serializing_none]
642#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
643pub struct Thesis {
644 pub university: Option<String>,
646 pub department: Option<String>,
648 #[serde(rename = "type")]
650 pub kind: Option<String>,
651 pub date_submitted: Option<String>,
653 pub date_defended: Option<String>,
655}
656#[skip_serializing_none]
658#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
659pub struct TitleType {
660 pub id: Option<String>,
662 pub title: Option<Value>,
664}
665#[skip_serializing_none]
667#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
668pub struct Tombstone {
669 pub reason: String,
671 pub category: String,
673 #[validate(nested)]
675 pub removed_by: Owner,
676 pub timestamp: String,
678}
679impl fmt::Display for AccessLevel {
680 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
681 match self {
682 | AccessLevel::Public => write!(f, "public"),
683 | AccessLevel::Restricted => write!(f, "restricted"),
684 }
685 }
686}
687impl TryFrom<datacite::Record> for Record {
688 type Error = CrosswalkError;
689
690 fn try_from(record: datacite::Record) -> Result<Self, Self::Error> {
691 let mapping = datacite_to_invenio();
692 crosswalk::convert(&record, &mapping).map(|(record, _)| record)
693 }
694}
695impl TryFrom<&datacite::Record> for Record {
696 type Error = CrosswalkError;
697
698 fn try_from(record: &datacite::Record) -> Result<Self, Self::Error> {
699 Record::try_from(record.clone())
700 }
701}
702impl SchemaBuilder for Record {
703 fn build_from_fields(fields: &Fields) -> Result<Self, CrosswalkError> {
704 let identifier_str = fields.get_string("identifier").unwrap_or_default();
705 let mut metadata = Metadata {
706 resource_type: None,
707 title: fields.get_string_opt("title"),
708 publication_date: fields.get_number_opt("publication-year").map(|n| format!("{:04}-01-01", n as i32)),
709 creators: None,
710 additional_titles: None,
711 description: fields.get_string_opt("description"),
712 additional_descriptions: None,
713 rights: None,
714 copyright: None,
715 contributors: None,
716 subjects: None,
717 languages: None,
718 dates: None,
719 version: fields.get_string_opt("version"),
720 publisher: None,
721 identifiers: None,
722 related_identifiers: None,
723 sizes: None,
724 formats: None,
725 locations: None,
726 funding: None,
727 references: None,
728 };
729 if let Some(creator_names) = fields.get_string_vec_opt("creators") {
730 metadata.creators = Some(
731 creator_names
732 .into_iter()
733 .map(|name| Creator {
734 person_or_org: PersonOrOrganization {
735 kind: "personal".to_string(),
736 name: Some(name),
737 given_name: None,
738 family_name: None,
739 identifiers: None,
740 },
741 affiliations: None,
742 role: None,
743 })
744 .collect(),
745 );
746 }
747 if let Some(rt_str) = fields.get_string_opt("resource-type") {
748 metadata.resource_type = Some(ResourceType {
749 id: Some(rt_str),
750 title: None,
751 });
752 }
753 if let Some(lang_str) = fields.get_string_opt("language") {
754 metadata.languages = Some(vec![Language { id: lang_str }]);
755 }
756 if let Some(ids) = fields.get_string_vec_opt("alternate-identifiers") {
757 metadata.identifiers = Some(
758 ids.into_iter()
759 .map(|identifier| Identifier {
760 identifier,
761 scheme: "unknown".to_string(),
762 })
763 .collect(),
764 );
765 }
766 if let Some(subjects) = fields.get_string_vec_opt("subjects") {
767 metadata.subjects = Some(
768 subjects
769 .into_iter()
770 .map(|subject| Subject {
771 id: None,
772 subject: Some(subject),
773 })
774 .collect(),
775 );
776 }
777 if let Some(license_iri) = fields.get_iri_opt("license") {
778 metadata.rights = Some(vec![Right {
779 id: None,
780 title: None,
781 description: None,
782 link: Some(license_iri),
783 }]);
784 }
785 if let Some(contributor_names) = fields.get_string_vec_opt("contributors") {
786 metadata.contributors = Some(
787 contributor_names
788 .into_iter()
789 .map(|name| Contributor {
790 person_or_org: PersonOrOrganization {
791 kind: "personal".to_string(),
792 name: Some(name),
793 given_name: None,
794 family_name: None,
795 identifiers: None,
796 },
797 role: ControlledVocabularyValue {
798 id: "contributor".to_string(),
799 },
800 affiliations: None,
801 })
802 .collect(),
803 );
804 }
805 Ok(Record {
806 schema: Some("https://inveniordm.docs.cern.ch/reference/metadata/".to_string()),
807 id: Some(identifier_str.clone()),
808 pid: Some(PersistentIdentifier {
809 pk: None,
810 status: Some("U".to_string()),
811 }),
812 pids: None,
813 parent: None,
814 access: None,
815 metadata: Some(metadata),
816 custom_fields: None,
817 files: None,
818 tombstone: None,
819 created: fields.get_date_opt("created"),
820 updated: fields.get_date_opt("updated"),
821 })
822 }
823}
824impl SchemaExtractor for Record {
825 fn extract_fields(&self) -> Fields {
826 let mut fields = Fields::new();
827 if let Some(pid) = &self.pid {
828 if let Some(pk) = pid.pk {
829 fields.insert("identifier", FieldValue::String(pk.to_string()));
830 }
831 }
832 let has_id = fields.contains_key("identifier");
833 if !has_id {
834 if let Some(id) = &self.id {
835 fields.insert("identifier", FieldValue::String(id.clone()));
836 }
837 }
838 if let Some(metadata) = &self.metadata {
839 if let Some(title) = &metadata.title {
841 fields.insert("title", FieldValue::String(title.clone()));
842 }
843 if let Some(creators) = &metadata.creators {
844 if !creators.is_empty() {
845 let creator_names: Vec<String> = creators.iter().filter_map(|c| c.person_or_org.name.clone()).collect();
846 if !creator_names.is_empty() {
847 fields.insert("creators", FieldValue::StringVec(creator_names));
848 }
849 }
850 }
851 if let Some(pub_date) = &metadata.publication_date {
852 if let Some(year_str) = pub_date.split('-').next() {
853 if let Ok(year) = year_str.parse::<f64>() {
854 fields.insert("publication-year", FieldValue::Number(year));
855 }
856 }
857 }
858 if let Some(resource_type) = &metadata.resource_type {
859 if let Some(id) = &resource_type.id {
860 fields.insert("resource-type", FieldValue::String(id.clone()));
861 }
862 }
863 if let Some(description) = &metadata.description {
864 fields.insert("description", FieldValue::String(description.clone()));
865 }
866 if let Some(languages) = &metadata.languages {
867 if let Some(first) = languages.first() {
868 fields.insert("language", FieldValue::String(first.id.clone()));
869 }
870 }
871 if let Some(identifiers) = &metadata.identifiers {
872 if !identifiers.is_empty() {
873 let id_strings: Vec<String> = identifiers.iter().map(|id| id.identifier.clone()).collect();
874 fields.insert("alternate-identifiers", FieldValue::StringVec(id_strings));
875 }
876 }
877 if let Some(subjects) = &metadata.subjects {
878 if !subjects.is_empty() {
879 let subject_strings: Vec<String> = subjects.iter().filter_map(|s| s.subject.clone()).collect();
880 if !subject_strings.is_empty() {
881 fields.insert("subjects", FieldValue::StringVec(subject_strings));
882 }
883 }
884 }
885 if let Some(rights) = &metadata.rights {
886 if let Some(first) = rights.first() {
887 if let Some(link) = &first.link {
888 fields.insert("license", FieldValue::IRI(link.clone()));
889 }
890 }
891 }
892 if let Some(contributors) = &metadata.contributors {
893 if !contributors.is_empty() {
894 let contributor_names: Vec<String> = contributors.iter().filter_map(|c| c.person_or_org.name.clone()).collect();
895 if !contributor_names.is_empty() {
896 fields.insert("contributors", FieldValue::StringVec(contributor_names));
897 }
898 }
899 }
900 }
901 if let Some(created) = &self.created {
902 fields.insert("created", FieldValue::Date(created.clone()));
903 }
904 if let Some(updated) = &self.updated {
905 fields.insert("updated", FieldValue::Date(updated.clone()));
906 }
907 fields
908 }
909}
910impl ToProse for Record {
911 fn to_prose(&self) -> String {
912 self.metadata
913 .iter()
914 .flat_map(|metadata| metadata.title.iter().cloned())
915 .chain(self.metadata.iter().flat_map(|metadata| metadata.description.iter().cloned()))
916 .chain(
917 self.metadata
918 .iter()
919 .flat_map(|metadata| metadata.additional_descriptions.iter().flatten().map(|value| value.description.clone())),
920 )
921 .collect::<Vec<String>>()
922 .join("\n\n")
923 }
924}
925#[cfg(feature = "std")]
926impl InputOutput for Record {
927 fn read(path: impl Into<PathBuf>) -> ApiResult<Record> {
928 let source = path.into();
929 match MimeType::from(source.display().to_string()) {
930 | MimeType::Json => Record::read_json(source),
931 | MimeType::Yaml => Record::read_yaml(source),
932 | _ => Err(eyre!("Unsupported Invenio data file extension")),
933 }
934 }
935 fn read_json(path: PathBuf) -> ApiResult<Record> {
936 #[derive(Deserialize)]
937 #[serde(untagged)]
938 enum JsonInput {
939 One(Box<Record>),
940 Many(Vec<Record>),
941 }
942
943 read_file(path).and_then(|content| {
944 serde_json::from_str::<JsonInput>(&content)
945 .map_err(|why| eyre!("Failed to parse JSON Invenio record — {why}"))
946 .and_then(|value| match value {
947 | JsonInput::One(record) => Ok(*record),
948 | JsonInput::Many(records) => match records.len() {
949 | 1 => records
950 .into_iter()
951 .next()
952 .ok_or_else(|| eyre!("Expected one Invenio record but found none")),
953 | len => Err(eyre!("Expected one Invenio record but found {len}")),
954 },
955 })
956 })
957 }
958 fn read_yaml(path: PathBuf) -> ApiResult<Record> {
959 #[derive(Deserialize)]
960 #[serde(untagged)]
961 enum YamlInput {
962 One(Box<Record>),
963 Many(Vec<Record>),
964 }
965
966 read_file(path).and_then(|content| {
967 serde_norway::from_str::<YamlInput>(&content)
968 .map_err(|why| eyre!("Failed to parse YAML Invenio record — {why}"))
969 .and_then(|value| match value {
970 | YamlInput::One(record) => Ok(*record),
971 | YamlInput::Many(records) => match records.len() {
972 | 1 => records
973 .into_iter()
974 .next()
975 .ok_or_else(|| eyre!("Expected one Invenio record but found none")),
976 | len => Err(eyre!("Expected one Invenio record but found {len}")),
977 },
978 })
979 })
980 }
981 fn write(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
982 let output = path.into();
983 match MimeType::from(output.display().to_string()) {
984 | MimeType::Json => self.write_json(output),
985 | MimeType::Yaml => self.write_yaml(output),
986 | _ => Err(eyre!("Unsupported Invenio data file extension for writing")),
987 }
988 }
989 fn write_json(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
990 let output = path.into().with_extension("json");
991 serde_json::to_string_pretty(self)
992 .map_err(|why| eyre!("Failed to serialize JSON Invenio record — {why}"))
993 .and_then(|content| write_file(output, content))
994 }
995 fn write_yaml(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
996 let output = path.into().with_extension("yaml");
997 serde_norway::to_string(self)
998 .map_err(|why| eyre!("Failed to serialize YAML Invenio record — {why}"))
999 .and_then(|content| write_file(output, content))
1000 }
1001}