1#[cfg(feature = "std")]
7use crate::io::current_date;
8#[cfg(feature = "std")]
9use crate::io::{read_file, License};
10use crate::prelude::*;
11#[cfg(feature = "std")]
12use crate::prelude::{Error, PathBuf};
13#[cfg(feature = "std")]
14use crate::schema::namespaces::{DEFAULT_ORCID_SCHEMA_URI, DEFAULT_ROR_SCHEMA_URI};
15#[cfg(feature = "std")]
16use crate::schema::research_activity::ResearchActivity;
17use crate::schema::validate::{has_at_least_one_truthy, is_date, is_orcid, is_raid, is_ror, is_unix_epoch, is_year};
18use crate::schema::Date;
19use crate::util::Constant;
20#[cfg(feature = "std")]
21use crate::util::Label;
22#[cfg(not(feature = "std"))]
23use crate::util::License;
24use alloc::collections::BTreeSet;
25use bon::Builder;
26use derive_more::Display;
27#[cfg(feature = "std")]
28use schemars::schema_for;
29use schemars::JsonSchema;
30use serde::{Deserialize, Serialize};
31use serde_with::skip_serializing_none;
32#[cfg(feature = "std")]
33use tracing::error;
34use validator::{Validate, ValidationError};
35
36pub struct Extractor<'a> {
38 records: &'a [Metadata],
39}
40impl<'a> Extractor<'a> {
41 pub fn new(records: &'a [Metadata]) -> Self {
43 Self { records }
44 }
45 fn titles(&self) -> impl Iterator<Item = &Title> {
46 self.records.iter().flat_map(|record| record.title.iter().flatten())
47 }
48 pub fn title(&self) -> Option<String> {
50 self.titles()
51 .filter(|title| title.title_type.as_ref().is_some_and(|kind| matches!(kind.id, TitleType::Primary)))
52 .find_map(|title| title.text.as_ref().filter(|value| !value.trim().is_empty()))
53 .cloned()
54 .or_else(|| {
55 self.titles()
56 .find_map(|title| title.text.as_ref().filter(|value| !value.trim().is_empty()))
57 .cloned()
58 })
59 }
60 pub fn contact(&self) -> Option<(Option<String>, Option<String>)> {
62 let contacts = self
63 .records
64 .iter()
65 .flat_map(|record| record.contributor.iter().flatten())
66 .filter(|contributor| contributor.contact)
67 .map(|contributor| {
68 (
69 contributor.id.clone().filter(|value| !value.trim().is_empty()),
70 contributor.email.clone().filter(|value| !value.trim().is_empty()),
71 )
72 })
73 .filter(|(identifier, email)| identifier.is_some() || email.is_some())
74 .collect::<BTreeSet<_>>();
75 (contacts.len() == 1).then(|| contacts.into_iter().next()).flatten()
76 }
77 pub fn organization_names(&self, sponsors: bool) -> Vec<String> {
79 self.records
80 .iter()
81 .flat_map(|record| {
82 let organizations = record.organization.as_deref().unwrap_or_default();
83 organizations.iter().filter(move |organization| {
84 let is_funder = organization.role.iter().any(|role| matches!(role.id, OrganizationRoleType::Funder));
85 let is_partner = (organizations.len() == 1 && organization.role.is_empty())
86 || organization.role.iter().any(|role| !matches!(role.id, OrganizationRoleType::Funder));
87 match sponsors {
88 | true => is_funder,
89 | false => is_partner,
90 }
91 })
92 })
93 .filter_map(|organization| {
94 let vocabularies = match sponsors {
95 | true => ["sponsors", "partners"],
96 | false => ["partners", "sponsors"],
97 };
98 let ror = organization.id.trim_end_matches('/').rsplit('/').next().unwrap_or(&organization.id);
99 vocabularies.iter().flat_map(Constant::csv).find_map(|row| match row.as_slice() {
100 | [name, _, value, ..] if value.eq_ignore_ascii_case(ror) => Some(name.clone()),
101 | _ => None,
102 })
103 })
104 .collect::<BTreeSet<_>>()
105 .into_iter()
106 .collect()
107 }
108}
109
110#[derive(Clone, Debug, Default, Deserialize, Display, JsonSchema, Serialize)]
112pub enum AccessType {
113 #[default]
115 #[display("open-access")]
116 #[serde(rename = "https://vocabularies.coar-repositories.org/access_rights/c_abf2/")]
117 OpenAccess,
118 #[display("embargoed-access")]
120 #[serde(rename = "https://vocabularies.coar-repositories.org/access_rights/c_f1cf/")]
121 EmbargoedAccess,
122}
123#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
129#[serde(rename_all = "kebab-case")]
130pub enum CreditRole {
131 #[display("conceptualization")]
133 #[serde(rename = "https://credit.niso.org/contributor-roles/conceptualization/")]
134 Conceptualization,
135 #[display("data-curation")]
137 #[serde(rename = "https://credit.niso.org/contributor-roles/data-curation/")]
138 DataCuration,
139 #[display("formal-analysis")]
141 #[serde(rename = "https://credit.niso.org/contributor-roles/formal-analysis/")]
142 FormalAnalysis,
143 #[display("funding-acquisition")]
145 #[serde(rename = "https://credit.niso.org/contributor-roles/funding-acquisition/")]
146 FundingAcquisition,
147 #[display("investigation")]
149 #[serde(rename = "https://credit.niso.org/contributor-roles/investigation/")]
150 Investigation,
151 #[display("methodology")]
153 #[serde(rename = "https://credit.niso.org/contributor-roles/methodology/")]
154 Methodology,
155 #[display("project-administration")]
157 #[serde(rename = "https://credit.niso.org/contributor-roles/project-administration/")]
158 ProjectAdministration,
159 #[display("resources")]
161 #[serde(rename = "https://credit.niso.org/contributor-roles/resources/")]
162 Resources,
163 #[display("software")]
165 #[serde(rename = "https://credit.niso.org/contributor-roles/software/")]
166 Software,
167 #[display("supervision")]
169 #[serde(rename = "https://credit.niso.org/contributor-roles/supervision/")]
170 Supervision,
171 #[display("validation")]
173 #[serde(rename = "https://credit.niso.org/contributor-roles/validation/")]
174 Validation,
175 #[display("visualization")]
177 #[serde(rename = "https://credit.niso.org/contributor-roles/visualization/")]
178 Visualization,
179 #[display("writing-original-draft")]
181 #[serde(rename = "https://credit.niso.org/contributor-roles/writing-original-draft/")]
182 WritingOriginalDraft,
183 #[display("writing-review-editing")]
185 #[serde(rename = "https://credit.niso.org/contributor-roles/writing-review-editing/")]
186 WritingReviewEditing,
187}
188#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
190#[serde(rename_all = "kebab-case")]
191pub enum DescriptionType {
192 #[display("primary")]
194 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/318")]
195 Primary,
196 #[display("alternative")]
198 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/319")]
199 Alternative,
200 #[display("brief")]
202 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/3")]
203 Brief,
204 #[display("significance-statement")]
206 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/9")]
207 SignificanceStatement,
208 #[display("methods")]
210 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/8")]
211 Methods,
212 #[display("objectives")]
214 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/7")]
215 Objectives,
216 #[display("acknowledgements")]
218 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/392")]
219 Acknowledgements,
220 #[display("other")]
222 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/6")]
223 Other,
224}
225#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
227#[serde(rename_all = "kebab-case")]
228pub enum ObjectCategoryType {
229 #[display("output")]
231 #[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/190")]
232 Output,
233 #[display("input")]
235 #[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/191")]
236 Input,
237 #[display("internal-process-document")]
239 #[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/192")]
240 InternalProcessDocument,
241}
242#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
244#[serde(rename_all = "kebab-case")]
245pub enum ObjectType {
246 #[display("output-management-plan")]
248 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/247")]
249 OutputManagementPlan,
250 #[display("conference-poster")]
252 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/248")]
253 ConferencePoster,
254 #[display("workflow")]
256 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/249")]
257 Workflow,
258 #[display("journal-article")]
260 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/250")]
261 JournalArticle,
262 #[display("standard")]
264 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/251")]
265 Standard,
266 #[display("report")]
268 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/252")]
269 Report,
270 #[display("dissertation")]
272 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/253")]
273 Dissertation,
274 #[display("preprint")]
276 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/254")]
277 Preprint,
278 #[display("data-paper")]
280 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/255")]
281 DataPaper,
282 #[display("computational-notebook")]
284 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/256")]
285 ComputationalNotebook,
286 #[display("image")]
288 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/257")]
289 Image,
290 #[display("book")]
292 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/258")]
293 Book,
294 #[display("software")]
296 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/259")]
297 Software,
298 #[display("event")]
300 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/260")]
301 Event,
302 #[display("sound")]
304 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/261")]
305 Sound,
306 #[display("conference-proceeding")]
308 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/262")]
309 ConferenceProceeding,
310 #[display("model")]
312 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/263")]
313 Model,
314 #[display("conference-paper")]
316 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/264")]
317 ConferencePaper,
318 #[display("text")]
320 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/265")]
321 Text,
322 #[display("instrument")]
324 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/266")]
325 Instrument,
326 #[display("learning-object")]
328 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/267")]
329 LearningObject,
330 #[display("prize")]
332 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/268")]
333 Prize,
334 #[display("dataset")]
336 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/269")]
337 Dataset,
338 #[display("physical-object")]
340 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/270")]
341 PhysicalObject,
342 #[display("book-chapter")]
344 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/271")]
345 BookChapter,
346 #[display("funding")]
350 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/272")]
351 Funding,
352 #[display("audiovisual")]
354 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/273")]
355 Audiovisual,
356 #[display("service")]
358 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/274")]
359 Service,
360}
361#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
363#[serde(rename_all = "kebab-case")]
364pub enum OrganizationRoleType {
365 #[display("lead-research-organization")]
367 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/182")]
368 LeadResearchOrganization,
369 #[display("other-research-organization")]
371 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/183")]
372 OtherResearchOrganization,
373 #[display("partner-organization")]
375 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/184")]
376 PartnerOrganization,
377 #[display("contractor")]
379 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/185")]
380 Contractor,
381 #[display("funder")]
383 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/186")]
384 Funder,
385 #[display("facility")]
387 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/187")]
388 Facility,
389 #[display("other-organization")]
391 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/188")]
392 OtherOrganization,
393}
394#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
398#[serde(rename_all = "kebab-case")]
399pub enum PositionType {
400 #[display("principal-investigator")]
402 #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/307")]
403 PrincipalInvestigator,
404 #[display("co-investigator")]
406 #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/308")]
407 CoInvestigator,
408 #[display("partner-investigator")]
410 #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/309")]
411 PartnerInvestigator,
412 #[display("consultant")]
414 #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/310")]
415 Consultant,
416 #[display("other")]
418 #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/311")]
419 Other,
420}
421#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
425#[serde(rename_all = "kebab-case")]
426pub enum RelatedRaidType {
427 #[display("obsoletes")]
430 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/198")]
431 Obsoletes,
432 #[display("is-source-of")]
434 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/199")]
435 IsSourceOf,
436 #[display("is-derived-from")]
438 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/200")]
439 IsDerivedFrom,
440 #[display("has-part")]
442 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/201")]
443 HasPart,
444 #[display("is-part-of")]
446 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/202")]
447 IsPartOf,
448 #[display("is-continued-by")]
450 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/203")]
451 IsContinuedBy,
452 #[display("continues")]
454 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/204")]
455 Continues,
456 #[display("is-obsoleted-by")]
459 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/205")]
460 IsObsoletedBy,
461}
462#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
464pub enum TitleType {
465 #[display("acronym")]
467 #[serde(rename = "https://vocabulary.raid.org/title.type.schema/156")]
468 Acronym,
469 #[display("alternative")]
471 #[serde(rename = "https://vocabulary.raid.org/title.type.schema/4")]
472 Alternative,
473 #[display("primary")]
475 #[serde(rename = "https://vocabulary.raid.org/title.type.schema/5")]
476 Primary,
477 #[display("short")]
479 #[serde(rename = "https://vocabulary.raid.org/title.type.schema/157")]
480 Short,
481}
482#[skip_serializing_none]
484#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
485#[builder(start_fn = init)]
486#[serde(deny_unknown_fields, rename_all = "camelCase")]
487pub struct Access {
488 #[validate(required, nested)]
490 #[serde(rename = "type")]
491 pub access_type: Option<AccessIdentifier>,
492 #[validate(custom(function = "is_date"))]
502 pub embargo_expiry: Option<String>,
503 #[validate(nested)]
507 pub statement: Option<AccessStatement>,
508}
509#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
511#[builder(start_fn = init, on(String, into))]
512#[serde(deny_unknown_fields, rename_all = "camelCase")]
513pub struct AccessIdentifier {
514 pub id: AccessType,
516 #[builder(default = "https://vocabularies.coar-repositories.org/access_rights/".to_string())]
518 #[validate(url)]
519 pub schema_uri: String,
520}
521#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
523#[serde(deny_unknown_fields, rename_all = "camelCase")]
524pub struct AccessStatement {
525 #[validate(length(min = 1, max = 1000))]
527 pub text: Option<String>,
528 #[validate(nested)]
530 pub language: Option<Language>,
531}
532#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
534#[serde(deny_unknown_fields, rename_all = "camelCase")]
535pub struct AlternateIdentifier {
536 pub id: String,
542 #[serde(rename = "type")]
544 pub alternate_identifier_type: String,
545}
546#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
548#[serde(deny_unknown_fields, rename_all = "camelCase")]
549pub struct AlternateUrl {
550 #[validate(url)]
551 url: String,
552}
553impl AlternateUrl {
554 pub fn url(&self) -> &str {
556 &self.url
557 }
558}
559#[skip_serializing_none]
563#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
564#[builder(start_fn = init, on(String, into))]
565#[serde(deny_unknown_fields, rename_all = "camelCase")]
566pub struct Contributor {
567 #[validate(required, custom(function = "is_orcid"))]
573 pub id: Option<String>,
574 #[validate(url)]
581 pub schema_uri: Option<String>,
582 pub status: Option<String>,
585 pub status_message: Option<String>,
587 #[validate(nested)]
590 pub position: Vec<ContributorPosition>,
591 #[builder(default = false)]
593 pub leader: bool,
594 #[builder(default = false)]
596 pub contact: bool,
597 #[validate(email)]
600 pub email: Option<String>,
601 #[validate(nested)]
603 pub role: Option<Vec<Role>>,
604 pub uuid: Option<String>,
607}
608#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
612#[builder(start_fn = init, on(String, into))]
613#[serde(deny_unknown_fields, rename_all = "camelCase")]
614pub struct ContributorPosition {
615 pub id: PositionType,
619 #[builder(default = "https://vocabulary.raid.org/contributor.position.schema/305".to_string())]
626 #[validate(url)]
627 pub schema_uri: String,
628 #[validate(custom(function = "has_start_date"), nested)]
630 #[serde(flatten)]
631 pub date: Date,
632}
633#[skip_serializing_none]
637#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
638#[builder(start_fn = init, on(String, into))]
639#[serde(deny_unknown_fields, rename_all = "camelCase")]
640pub struct Description {
641 #[validate(required, length(min = 3, max = 1000))]
643 pub text: Option<String>,
644 #[validate(required, nested)]
646 #[serde(rename = "type")]
647 pub description_type: Option<DescriptionIdentifier>,
648 #[validate(nested)]
650 pub language: Option<Language>,
651}
652#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
654#[builder(start_fn = init, on(String, into))]
655#[serde(deny_unknown_fields, rename_all = "camelCase")]
656pub struct DescriptionIdentifier {
657 pub id: DescriptionType,
659 #[builder(default = "https://vocabulary.raid.org/description.type.schema/320".to_string())]
661 #[validate(url)]
662 pub schema_uri: String,
663}
664#[derive(Builder, Clone, Debug, Deserialize, Serialize, JsonSchema, Validate)]
666#[builder(start_fn = init, on(String, into))]
667#[serde(deny_unknown_fields, rename_all = "camelCase")]
668pub struct Identifier {
669 pub id: String,
671 #[validate(url)]
673 pub schema_uri: Option<String>,
674}
675#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
677#[builder(start_fn = init, on(String, into))]
678#[serde(deny_unknown_fields, rename_all = "camelCase")]
679pub struct Keyword {
680 pub text: String,
682 #[validate(nested)]
684 pub language: Option<Language>,
685}
686#[derive(Builder, Clone, Debug, Deserialize, Serialize, JsonSchema, Validate)]
688#[builder(start_fn = init, on(String, into))]
689#[serde(deny_unknown_fields, rename_all = "camelCase")]
690pub struct Language {
691 #[validate(length(equal = 3))]
699 pub id: String,
700 #[validate(url)]
702 pub schema_uri: Option<String>,
703}
704#[skip_serializing_none]
706#[derive(Builder, Clone, Debug, Display, eserde::Deserialize, Serialize, JsonSchema, Validate)]
707#[builder(start_fn = init)]
708#[display("({identifier:?})")]
709#[validate(schema(function = "validate_metadata", skip_on_field_errors = false))]
710#[serde(deny_unknown_fields, rename_all = "camelCase")]
711pub struct Metadata {
712 #[validate(required, nested)]
714 #[eserde(compat)]
715 pub access: Option<Access>,
716 #[validate(required, nested, length(min = 1))]
718 #[eserde(compat)]
719 pub contributor: Option<Vec<Contributor>>,
720 #[validate(required, custom(function = "has_start_date"), nested)]
722 #[eserde(compat)]
723 pub date: Option<Date>,
724 #[validate(nested)]
726 #[eserde(compat)]
727 pub identifier: Option<MetadataIdentifier>,
728 #[validate(required, nested, length(min = 1))]
732 #[eserde(compat)]
733 pub title: Option<Vec<Title>>,
734 #[validate(nested)]
736 #[eserde(compat)]
737 pub alternate_identifier: Option<Vec<AlternateIdentifier>>,
738 #[validate(nested)]
740 #[eserde(compat)]
741 pub alternate_url: Option<Vec<AlternateUrl>>,
742 #[validate(nested)]
744 #[eserde(compat)]
745 pub description: Option<Vec<Description>>,
746 #[validate(nested)]
748 #[eserde(compat)]
749 pub metadata: Option<MetadataMetadata>,
750 #[validate(nested)]
756 #[serde(alias = "organisation")]
757 #[eserde(compat)]
758 pub organization: Option<Vec<Organization>>,
759 #[validate(nested)]
761 #[eserde(compat)]
762 pub related_object: Option<Vec<RelatedObject>>,
763 #[validate(nested)]
765 #[eserde(compat)]
766 pub related_raid: Option<Vec<RelatedRaid>>,
767 #[validate(nested)]
769 #[eserde(compat)]
770 pub spatial_coverage: Option<Vec<SpatialCoverage>>,
771 #[validate(nested)]
773 #[eserde(compat)]
774 pub subject: Option<Vec<Subject>>,
775 #[validate(nested)]
777 #[eserde(compat)]
778 pub traditional_knowledge_label: Option<Vec<TraditionalKnowledgeLabel>>,
779}
780#[derive(Builder, Clone, Debug, Serialize, Deserialize, Display, JsonSchema, Validate)]
784#[display("{id:?}")]
785#[serde(deny_unknown_fields, rename_all = "camelCase")]
786pub struct MetadataIdentifier {
787 #[validate(required, custom(function = "is_raid"))]
791 pub id: Option<String>,
792 #[validate(required, url)]
796 pub schema_uri: Option<String>,
797 #[validate(required, nested)]
799 pub owner: Option<Owner>,
800 #[validate(required, url)]
802 pub raid_agency_url: Option<String>,
803 #[validate(required, nested)]
805 pub registration_agency: Option<RegistrationAgency>,
806 #[validate(required, nested)]
810 pub license: Option<License>,
811 #[validate(required, range(min = 0))]
813 pub version: Option<u32>,
814}
815#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
817#[serde(deny_unknown_fields, rename_all = "camelCase")]
818pub struct MetadataMetadata {
819 #[validate(custom(function = "is_unix_epoch"))]
823 pub created: usize,
824 #[validate(custom(function = "is_unix_epoch"))]
828 pub updated: usize,
829}
830#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
834#[serde(deny_unknown_fields, rename_all = "camelCase")]
835pub struct Organization {
836 #[validate(custom(function = "is_ror"))]
842 pub id: String,
843 #[validate(url, contains(pattern = "https://ror.org"))]
847 pub schema_uri: Option<String>,
848 #[validate(nested)]
850 pub role: Vec<OrganizationRole>,
851}
852#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
854#[serde(deny_unknown_fields, rename_all = "camelCase")]
855pub struct OrganizationRole {
856 pub id: OrganizationRoleType,
858 #[validate(url)]
860 pub schema_uri: Option<String>,
861 #[validate(custom(function = "has_start_date"), nested)]
863 #[serde(flatten)]
864 pub date: Date,
865}
866#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
870#[serde(deny_unknown_fields, rename_all = "camelCase")]
871pub struct Owner {
872 #[validate(custom(function = "is_ror"))]
878 pub id: String,
879 #[validate(url)]
883 pub schema_uri: Option<String>,
884 pub service_point: usize,
892}
893#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
895#[serde(deny_unknown_fields, rename_all = "camelCase")]
896pub struct Place {
897 pub text: Option<String>,
901 #[validate(nested)]
903 pub language: Option<Language>,
904}
905#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
907#[serde(deny_unknown_fields, rename_all = "camelCase")]
908pub struct RelatedObject {
909 pub id: String,
916 #[validate(url)]
918 pub schema_uri: Option<String>,
919 #[validate(nested)]
921 #[serde(rename = "type")]
922 pub related_object_type: RelatedObjectIdentifier,
923 #[validate(nested, length(min = 1))]
925 pub category: Vec<RelatedObjectCategory>,
926}
927#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
929#[serde(deny_unknown_fields, rename_all = "camelCase")]
930pub struct RelatedObjectCategory {
931 pub id: ObjectCategoryType,
933 #[validate(url)]
935 pub schema_uri: Option<String>,
936}
937#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
939#[serde(deny_unknown_fields, rename_all = "camelCase")]
940pub struct RelatedObjectIdentifier {
941 pub id: ObjectType,
943 #[validate(url)]
945 pub schema_uri: Option<String>,
946}
947#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
949#[serde(deny_unknown_fields, rename_all = "camelCase")]
950pub struct RelatedRaid {
951 pub id: String,
953 #[validate(nested)]
955 #[serde(rename = "type")]
956 pub related_raid_type: RelatedRaidIdentifier,
957}
958#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
960#[serde(deny_unknown_fields, rename_all = "camelCase")]
961pub struct RelatedRaidIdentifier {
962 pub id: RelatedRaidType,
964 #[validate(url)]
966 pub schema_uri: Option<String>,
967}
968#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
972#[serde(deny_unknown_fields, rename_all = "camelCase")]
973pub struct RegistrationAgency {
974 #[validate(custom(function = "is_ror"))]
978 pub id: String,
979 #[validate(url)]
983 pub schema_uri: Option<String>,
984}
985#[skip_serializing_none]
991#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
992#[builder(start_fn = init, on(String, into))]
993#[serde(deny_unknown_fields, rename_all = "camelCase")]
994pub struct Role {
995 #[validate(required)]
997 pub id: Option<CreditRole>,
998 #[builder(default = "https://credit.niso.org/".to_string())]
1000 #[validate(url)]
1001 pub schema_uri: String,
1002}
1003#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
1007#[serde(deny_unknown_fields, rename_all = "camelCase")]
1008pub struct SpatialCoverage {
1009 pub id: String,
1011 #[validate(url)]
1013 pub schema_uri: Option<String>,
1014 #[validate(nested)]
1016 pub place: Vec<Place>,
1017}
1018#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
1022#[serde(deny_unknown_fields, rename_all = "camelCase")]
1023pub struct Subject {
1024 pub id: String,
1026 #[validate(url)]
1028 pub schema_uri: Option<String>,
1029 #[validate(required, nested)]
1031 pub keyword: Option<Vec<Keyword>>,
1032}
1033#[skip_serializing_none]
1037#[derive(Builder, Clone, Debug, Display, Serialize, Deserialize, JsonSchema, Validate)]
1038#[builder(start_fn = init, on(String, into))]
1039#[display("{text:?} ({title_type:?})")]
1040#[serde(deny_unknown_fields, rename_all = "camelCase")]
1041pub struct Title {
1042 #[validate(required, length(min = 3, max = 100))]
1044 pub text: Option<String>,
1045 #[validate(required, nested)]
1047 #[serde(rename = "type")]
1048 pub title_type: Option<TitleIdentifier>,
1049 #[validate(nested)]
1051 pub language: Option<Language>,
1052 #[validate(required, custom(function = "is_date"))]
1058 pub start_date: Option<String>,
1059 #[validate(custom(function = "is_year"))]
1069 pub end_date: Option<String>,
1070}
1071#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
1075#[serde(deny_unknown_fields, rename_all = "camelCase")]
1076pub struct TraditionalKnowledgeLabel {
1077 pub id: String,
1081 #[validate(url)]
1085 pub schema_uri: Option<String>,
1086}
1087#[derive(Builder, Clone, Debug, Serialize, Deserialize, Display, JsonSchema, Validate)]
1089#[builder(start_fn = init, on(String, into))]
1090#[display("{id}")]
1091#[serde(deny_unknown_fields, rename_all = "camelCase")]
1092pub struct TitleIdentifier {
1093 pub id: TitleType,
1097 #[builder(default = "https://vocabulary.raid.org/title.type.schema/376".to_string())]
1099 #[validate(url)]
1100 pub schema_uri: String,
1101}
1102impl Metadata {
1103 #[cfg(feature = "std")]
1105 pub fn to_schema(format: &str) {
1106 let schema = schema_for!(Metadata);
1107 let output = match format.to_lowercase().as_str() {
1108 | "yaml" | "yml" => serde_norway::to_string(&schema).unwrap_or_default(),
1109 | _ => serde_json::to_string_pretty(&schema).unwrap_or_default(),
1110 };
1111 println!("{output}");
1112 }
1113 #[cfg(feature = "std")]
1115 pub fn read(path: PathBuf) -> Result<Metadata, Error> {
1116 match read_file(path) {
1117 | Ok(data) => match eserde::json::from_str::<Metadata>(&data) {
1118 | Ok(value) => Ok(value),
1119 | Err(errors) => {
1120 let details: Vec<String> = errors
1121 .iter()
1122 .map(|e| format!("{}: {}", e.path().map_or("root".into(), |p| p.to_string()), e.message()))
1123 .collect();
1124 Err(Error::other(details.join("\n")))
1125 }
1126 },
1127 | Err(why) => {
1128 let msg = format!("Read RAiD metadata file - {why}");
1129 error!("=> {} {}", Label::fail(), msg);
1130 Err(Error::other(msg))
1131 }
1132 }
1133 }
1134}
1135impl Contributor {
1136 fn is_contact(&self) -> bool {
1137 self.contact
1138 }
1139 fn is_leader(&self) -> bool {
1140 self.leader
1141 }
1142}
1143impl Organization {
1144 fn is_lead_research_organization(&self) -> bool {
1145 self.role
1146 .iter()
1147 .any(|role| matches!(role.id, OrganizationRoleType::LeadResearchOrganization))
1148 }
1149}
1150#[cfg(feature = "std")]
1151impl From<ResearchActivity> for Metadata {
1152 fn from(activity: ResearchActivity) -> Self {
1153 let default_start_date = current_date();
1154 let default_end_year = default_start_date.chars().take(4).collect::<String>();
1155 let raid_id = activity.meta.raid.as_ref().and_then(|raid| raid.first()).cloned().unwrap_or_default();
1156 let ror_id = activity.meta.ror.as_ref().and_then(|rors| rors.first()).cloned().unwrap_or_default();
1157 let contributor_id = activity.contact.identifier.clone().filter(|id| is_orcid(id).is_ok()).unwrap_or_default();
1158 let date = Date {
1159 start_date: Some(default_start_date.clone()),
1160 end_date: None,
1161 };
1162 let contributor_position = ContributorPosition::init()
1163 .id(PositionType::PrincipalInvestigator)
1164 .date(date.clone())
1165 .build();
1166 let contributor = Contributor {
1167 id: Some(contributor_id),
1168 schema_uri: Some(DEFAULT_ORCID_SCHEMA_URI.to_string()),
1169 status: Some("active".to_string()),
1170 status_message: None,
1171 position: vec![contributor_position],
1172 leader: true,
1173 contact: true,
1174 email: Some(activity.contact.email.clone()),
1175 role: None,
1176 uuid: None,
1177 };
1178 let org_role = OrganizationRole {
1179 id: OrganizationRoleType::LeadResearchOrganization,
1180 schema_uri: None,
1181 date: date.clone(),
1182 };
1183 let organization = Organization {
1184 id: ror_id.clone(),
1185 schema_uri: Some(DEFAULT_ROR_SCHEMA_URI.to_string()),
1186 role: vec![org_role],
1187 };
1188 let title = Title {
1189 text: Some(activity.title.clone()),
1190 title_type: Some(TitleIdentifier::init().id(TitleType::Primary).build()),
1191 language: None,
1192 start_date: Some(default_start_date.clone()),
1193 end_date: Some(default_end_year.clone()),
1194 };
1195 let description = Description {
1196 text: Some(activity.sections.mission.clone()),
1197 description_type: Some(DescriptionIdentifier {
1198 id: DescriptionType::Primary,
1199 schema_uri: "https://vocabulary.raid.org/description.type.schema/318".to_string(),
1200 }),
1201 language: None,
1202 };
1203 let identifier = MetadataIdentifier {
1204 id: Some(raid_id),
1205 schema_uri: Some("https://raid.org/".to_string()),
1206 owner: Some(Owner {
1207 id: ror_id.clone(),
1208 schema_uri: Some(DEFAULT_ROR_SCHEMA_URI.to_string()),
1209 service_point: 0,
1210 }),
1211 raid_agency_url: Some("https://raid.org/".to_string()),
1212 registration_agency: Some(RegistrationAgency {
1213 id: ror_id,
1214 schema_uri: Some(DEFAULT_ROR_SCHEMA_URI.to_string()),
1215 }),
1216 license: Some(License::Single("CC0-1.0".to_string())),
1217 version: Some(1),
1218 };
1219 let access = Access {
1220 access_type: Some(AccessIdentifier {
1221 id: AccessType::OpenAccess,
1222 schema_uri: "https://vocabularies.coar-repositories.org/access_rights/".to_string(),
1223 }),
1224 embargo_expiry: None,
1225 statement: None,
1226 };
1227 Metadata {
1228 access: Some(access),
1229 contributor: Some(vec![contributor]),
1230 date: None,
1231 description: Some(vec![description]),
1232 metadata: None,
1233 title: Some(vec![title]),
1234 identifier: Some(identifier),
1235 alternate_identifier: None,
1236 alternate_url: None,
1237 organization: Some(vec![organization]),
1238 related_object: None,
1239 related_raid: None,
1240 spatial_coverage: None,
1241 subject: None,
1242 traditional_knowledge_label: None,
1243 }
1244 }
1245}
1246fn has_contact(value: &Vec<Contributor>) -> Result<(), ValidationError> {
1247 has_at_least_one_truthy(
1248 value.as_slice(),
1249 Contributor::is_contact,
1250 "contributors",
1251 "Mark at least one contributor as contact",
1252 )
1253}
1254fn has_leader(value: &Vec<Contributor>) -> Result<(), ValidationError> {
1255 has_at_least_one_truthy(
1256 value.as_slice(),
1257 Contributor::is_leader,
1258 "contributors",
1259 "Mark at least one contributor as leader",
1260 )
1261}
1262fn has_start_date(value: &Date) -> Result<(), ValidationError> {
1263 match value.start_date.as_ref() {
1264 | Some(start_date) if !start_date.trim().is_empty() => Ok(()),
1265 | _ => Err(ValidationError::new("date").with_message("Provide valid start date".into())),
1266 }
1267}
1268fn validate_contributors(value: &Metadata) -> Result<(), ValidationError> {
1269 match &value.contributor {
1270 | Some(contributors) => {
1271 let message = [
1272 has_contact(contributors)
1273 .err()
1274 .map(|_| "Mark at least one contributor as contact".to_string()),
1275 has_leader(contributors)
1276 .err()
1277 .map(|_| "Mark at least one contributor as leader".to_string()),
1278 ]
1279 .into_iter()
1280 .flatten()
1281 .collect::<Vec<String>>()
1282 .join("; ");
1283 match message.is_empty() {
1284 | true => Ok(()),
1285 | false => Err(ValidationError::new("contributors").with_message(message.into())),
1286 }
1287 }
1288 | None => Ok(()),
1289 }
1290}
1291fn validate_organization(value: &Metadata) -> Result<(), ValidationError> {
1292 match &value.organization {
1293 | Some(organizations) => {
1294 let lead_count = organizations
1295 .iter()
1296 .filter(|organization| organization.is_lead_research_organization())
1297 .count();
1298 let message = if organizations.is_empty() {
1299 None
1300 } else if lead_count == 0 {
1301 Some("Mark one organization as lead research organization".to_string())
1302 } else if lead_count > 1 {
1303 Some("Only one organization can be lead research organization".to_string())
1304 } else {
1305 None
1306 };
1307 match message {
1308 | Some(value) => Err(ValidationError::new("organization").with_message(value.into())),
1309 | None => Ok(()),
1310 }
1311 }
1312 | None => Ok(()),
1313 }
1314}
1315fn validate_metadata(value: &Metadata) -> Result<(), ValidationError> {
1316 let message = [
1317 validate_contributors(value)
1318 .err()
1319 .map(|why| why.message.unwrap_or_else(|| "Invalid contributors".into()).to_string()),
1320 validate_organization(value)
1321 .err()
1322 .map(|why| why.message.unwrap_or_else(|| "Invalid organization".into()).to_string()),
1323 ]
1324 .into_iter()
1325 .flatten()
1326 .collect::<Vec<String>>()
1327 .join("; ");
1328 match message.is_empty() {
1329 | true => Ok(()),
1330 | false => Err(ValidationError::new("metadata").with_message(message.into())),
1331 }
1332}