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;
19#[cfg(feature = "std")]
20use crate::util::Label;
21#[cfg(not(feature = "std"))]
22use crate::util::License;
23use bon::Builder;
24use derive_more::Display;
25#[cfg(feature = "std")]
26use schemars::schema_for;
27use schemars::JsonSchema;
28use serde::{Deserialize, Serialize};
29use serde_with::skip_serializing_none;
30#[cfg(feature = "std")]
31use tracing::error;
32use validator::{Validate, ValidationError};
33
34#[derive(Clone, Debug, Default, Deserialize, Display, JsonSchema, Serialize)]
36pub enum AccessType {
37 #[default]
39 #[display("open-access")]
40 #[serde(rename = "https://vocabularies.coar-repositories.org/access_rights/c_abf2/")]
41 OpenAccess,
42 #[display("embargoed-access")]
44 #[serde(rename = "https://vocabularies.coar-repositories.org/access_rights/c_f1cf/")]
45 EmbargoedAccess,
46}
47#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
53#[serde(rename_all = "kebab-case")]
54pub enum CreditRole {
55 #[display("conceptualization")]
57 #[serde(rename = "https://credit.niso.org/contributor-roles/conceptualization/")]
58 Conceptualization,
59 #[display("data-curation")]
61 #[serde(rename = "https://credit.niso.org/contributor-roles/data-curation/")]
62 DataCuration,
63 #[display("formal-analysis")]
65 #[serde(rename = "https://credit.niso.org/contributor-roles/formal-analysis/")]
66 FormalAnalysis,
67 #[display("funding-acquisition")]
69 #[serde(rename = "https://credit.niso.org/contributor-roles/funding-acquisition/")]
70 FundingAcquisition,
71 #[display("investigation")]
73 #[serde(rename = "https://credit.niso.org/contributor-roles/investigation/")]
74 Investigation,
75 #[display("methodology")]
77 #[serde(rename = "https://credit.niso.org/contributor-roles/methodology/")]
78 Methodology,
79 #[display("project-administration")]
81 #[serde(rename = "https://credit.niso.org/contributor-roles/project-administration/")]
82 ProjectAdministration,
83 #[display("resources")]
85 #[serde(rename = "https://credit.niso.org/contributor-roles/resources/")]
86 Resources,
87 #[display("software")]
89 #[serde(rename = "https://credit.niso.org/contributor-roles/software/")]
90 Software,
91 #[display("supervision")]
93 #[serde(rename = "https://credit.niso.org/contributor-roles/supervision/")]
94 Supervision,
95 #[display("validation")]
97 #[serde(rename = "https://credit.niso.org/contributor-roles/validation/")]
98 Validation,
99 #[display("visualization")]
101 #[serde(rename = "https://credit.niso.org/contributor-roles/visualization/")]
102 Visualization,
103 #[display("writing-original-draft")]
105 #[serde(rename = "https://credit.niso.org/contributor-roles/writing-original-draft/")]
106 WritingOriginalDraft,
107 #[display("writing-review-editing")]
109 #[serde(rename = "https://credit.niso.org/contributor-roles/writing-review-editing/")]
110 WritingReviewEditing,
111}
112#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
114#[serde(rename_all = "kebab-case")]
115pub enum DescriptionType {
116 #[display("primary")]
118 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/318")]
119 Primary,
120 #[display("alternative")]
122 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/319")]
123 Alternative,
124 #[display("brief")]
126 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/3")]
127 Brief,
128 #[display("significance-statement")]
130 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/9")]
131 SignificanceStatement,
132 #[display("methods")]
134 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/8")]
135 Methods,
136 #[display("objectives")]
138 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/7")]
139 Objectives,
140 #[display("acknowledgements")]
142 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/392")]
143 Acknowledgements,
144 #[display("other")]
146 #[serde(rename = "https://vocabulary.raid.org/description.type.schema/6")]
147 Other,
148}
149#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
151#[serde(rename_all = "kebab-case")]
152pub enum ObjectCategoryType {
153 #[display("output")]
155 #[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/190")]
156 Output,
157 #[display("input")]
159 #[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/191")]
160 Input,
161 #[display("internal-process-document")]
163 #[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/192")]
164 InternalProcessDocument,
165}
166#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
168#[serde(rename_all = "kebab-case")]
169pub enum ObjectType {
170 #[display("output-management-plan")]
172 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/247")]
173 OutputManagementPlan,
174 #[display("conference-poster")]
176 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/248")]
177 ConferencePoster,
178 #[display("workflow")]
180 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/249")]
181 Workflow,
182 #[display("journal-article")]
184 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/250")]
185 JournalArticle,
186 #[display("standard")]
188 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/251")]
189 Standard,
190 #[display("report")]
192 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/252")]
193 Report,
194 #[display("dissertation")]
196 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/253")]
197 Dissertation,
198 #[display("preprint")]
200 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/254")]
201 Preprint,
202 #[display("data-paper")]
204 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/255")]
205 DataPaper,
206 #[display("computational-notebook")]
208 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/256")]
209 ComputationalNotebook,
210 #[display("image")]
212 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/257")]
213 Image,
214 #[display("book")]
216 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/258")]
217 Book,
218 #[display("software")]
220 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/259")]
221 Software,
222 #[display("event")]
224 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/260")]
225 Event,
226 #[display("sound")]
228 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/261")]
229 Sound,
230 #[display("conference-proceeding")]
232 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/262")]
233 ConferenceProceeding,
234 #[display("model")]
236 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/263")]
237 Model,
238 #[display("conference-paper")]
240 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/264")]
241 ConferencePaper,
242 #[display("text")]
244 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/265")]
245 Text,
246 #[display("instrument")]
248 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/266")]
249 Instrument,
250 #[display("learning-object")]
252 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/267")]
253 LearningObject,
254 #[display("prize")]
256 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/268")]
257 Prize,
258 #[display("dataset")]
260 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/269")]
261 Dataset,
262 #[display("physical-object")]
264 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/270")]
265 PhysicalObject,
266 #[display("book-chapter")]
268 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/271")]
269 BookChapter,
270 #[display("funding")]
274 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/272")]
275 Funding,
276 #[display("audiovisual")]
278 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/273")]
279 Audiovisual,
280 #[display("service")]
282 #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/274")]
283 Service,
284}
285#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
287#[serde(rename_all = "kebab-case")]
288pub enum OrganizationRoleType {
289 #[display("lead-research-organization")]
291 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/182")]
292 LeadResearchOrganization,
293 #[display("other-research-organization")]
295 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/183")]
296 OtherResearchOrganization,
297 #[display("partner-organization")]
299 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/184")]
300 PartnerOrganization,
301 #[display("contractor")]
303 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/185")]
304 Contractor,
305 #[display("funder")]
307 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/186")]
308 Funder,
309 #[display("facility")]
311 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/187")]
312 Facility,
313 #[display("other-organization")]
315 #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/188")]
316 OtherOrganization,
317}
318#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
322#[serde(rename_all = "kebab-case")]
323pub enum PositionType {
324 #[display("principal-investigator")]
326 #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/307")]
327 PrincipalInvestigator,
328 #[display("co-investigator")]
330 #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/308")]
331 CoInvestigator,
332 #[display("partner-investigator")]
334 #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/309")]
335 PartnerInvestigator,
336 #[display("consultant")]
338 #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/310")]
339 Consultant,
340 #[display("other")]
342 #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/311")]
343 Other,
344}
345#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
349#[serde(rename_all = "kebab-case")]
350pub enum RelatedRaidType {
351 #[display("obsoletes")]
354 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/198")]
355 Obsoletes,
356 #[display("is-source-of")]
358 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/199")]
359 IsSourceOf,
360 #[display("is-derived-from")]
362 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/200")]
363 IsDerivedFrom,
364 #[display("has-part")]
366 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/201")]
367 HasPart,
368 #[display("is-part-of")]
370 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/202")]
371 IsPartOf,
372 #[display("is-continued-by")]
374 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/203")]
375 IsContinuedBy,
376 #[display("continues")]
378 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/204")]
379 Continues,
380 #[display("is-obsoleted-by")]
383 #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/205")]
384 IsObsoletedBy,
385}
386#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
388pub enum TitleType {
389 #[display("acronym")]
391 #[serde(rename = "https://vocabulary.raid.org/title.type.schema/156")]
392 Acronym,
393 #[display("alternative")]
395 #[serde(rename = "https://vocabulary.raid.org/title.type.schema/4")]
396 Alternative,
397 #[display("primary")]
399 #[serde(rename = "https://vocabulary.raid.org/title.type.schema/5")]
400 Primary,
401 #[display("short")]
403 #[serde(rename = "https://vocabulary.raid.org/title.type.schema/157")]
404 Short,
405}
406#[skip_serializing_none]
408#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
409#[builder(start_fn = init)]
410#[serde(deny_unknown_fields, rename_all = "camelCase")]
411pub struct Access {
412 #[validate(required, nested)]
414 #[serde(rename = "type")]
415 pub access_type: Option<AccessIdentifier>,
416 #[validate(custom(function = "is_date"))]
426 pub embargo_expiry: Option<String>,
427 #[validate(nested)]
431 pub statement: Option<AccessStatement>,
432}
433#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
435#[builder(start_fn = init, on(String, into))]
436#[serde(deny_unknown_fields, rename_all = "camelCase")]
437pub struct AccessIdentifier {
438 pub id: AccessType,
440 #[builder(default = "https://vocabularies.coar-repositories.org/access_rights/".to_string())]
442 #[validate(url)]
443 pub schema_uri: String,
444}
445#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
447#[serde(deny_unknown_fields, rename_all = "camelCase")]
448pub struct AccessStatement {
449 #[validate(length(min = 1, max = 1000))]
451 pub text: Option<String>,
452 #[validate(nested)]
454 pub language: Option<Language>,
455}
456#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
458#[serde(deny_unknown_fields, rename_all = "camelCase")]
459pub struct AlternateIdentifier {
460 pub id: String,
466 #[serde(rename = "type")]
468 pub alternate_identifier_type: String,
469}
470#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
472#[serde(deny_unknown_fields, rename_all = "camelCase")]
473pub struct AlternateUrl {
474 #[validate(url)]
475 url: String,
476}
477#[skip_serializing_none]
481#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
482#[builder(start_fn = init, on(String, into))]
483#[serde(deny_unknown_fields, rename_all = "camelCase")]
484pub struct Contributor {
485 #[validate(required, custom(function = "is_orcid"))]
491 pub id: Option<String>,
492 #[validate(url)]
499 pub schema_uri: Option<String>,
500 pub status: Option<String>,
503 pub status_message: Option<String>,
505 #[validate(nested)]
508 pub position: Vec<ContributorPosition>,
509 #[builder(default = false)]
511 pub leader: bool,
512 #[builder(default = false)]
514 pub contact: bool,
515 #[validate(email)]
518 pub email: Option<String>,
519 #[validate(nested)]
521 pub role: Option<Vec<Role>>,
522 pub uuid: Option<String>,
525}
526#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
530#[builder(start_fn = init, on(String, into))]
531#[serde(deny_unknown_fields, rename_all = "camelCase")]
532pub struct ContributorPosition {
533 pub id: PositionType,
537 #[builder(default = "https://vocabulary.raid.org/contributor.position.schema/305".to_string())]
544 #[validate(url)]
545 pub schema_uri: String,
546 #[validate(custom(function = "has_start_date"), nested)]
548 #[serde(flatten)]
549 pub date: Date,
550}
551#[skip_serializing_none]
555#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
556#[builder(start_fn = init, on(String, into))]
557#[serde(deny_unknown_fields, rename_all = "camelCase")]
558pub struct Description {
559 #[validate(required, length(min = 3, max = 1000))]
561 pub text: Option<String>,
562 #[validate(required, nested)]
564 #[serde(rename = "type")]
565 pub description_type: Option<DescriptionIdentifier>,
566 #[validate(nested)]
568 pub language: Option<Language>,
569}
570#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
572#[builder(start_fn = init, on(String, into))]
573#[serde(deny_unknown_fields, rename_all = "camelCase")]
574pub struct DescriptionIdentifier {
575 pub id: DescriptionType,
577 #[builder(default = "https://vocabulary.raid.org/description.type.schema/320".to_string())]
579 #[validate(url)]
580 pub schema_uri: String,
581}
582#[derive(Builder, Clone, Debug, Deserialize, Serialize, JsonSchema, Validate)]
584#[builder(start_fn = init, on(String, into))]
585#[serde(deny_unknown_fields, rename_all = "camelCase")]
586pub struct Identifier {
587 pub id: String,
589 #[validate(url)]
591 pub schema_uri: Option<String>,
592}
593#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
595#[builder(start_fn = init, on(String, into))]
596#[serde(deny_unknown_fields, rename_all = "camelCase")]
597pub struct Keyword {
598 pub text: String,
600 #[validate(nested)]
602 pub language: Option<Language>,
603}
604#[derive(Builder, Clone, Debug, Deserialize, Serialize, JsonSchema, Validate)]
606#[builder(start_fn = init, on(String, into))]
607#[serde(deny_unknown_fields, rename_all = "camelCase")]
608pub struct Language {
609 #[validate(length(equal = 3))]
617 pub id: String,
618 #[validate(url)]
620 pub schema_uri: Option<String>,
621}
622#[skip_serializing_none]
624#[derive(Builder, Clone, Debug, Display, eserde::Deserialize, Serialize, JsonSchema, Validate)]
625#[builder(start_fn = init)]
626#[display("({identifier:?})")]
627#[validate(schema(function = "validate_metadata", skip_on_field_errors = false))]
628#[serde(deny_unknown_fields, rename_all = "camelCase")]
629pub struct Metadata {
630 #[validate(required, nested)]
632 #[eserde(compat)]
633 pub access: Option<Access>,
634 #[validate(required, nested, length(min = 1))]
636 #[eserde(compat)]
637 pub contributor: Option<Vec<Contributor>>,
638 #[validate(required, custom(function = "has_start_date"), nested)]
640 #[eserde(compat)]
641 pub date: Option<Date>,
642 #[validate(nested)]
644 #[eserde(compat)]
645 pub identifier: Option<MetadataIdentifier>,
646 #[validate(required, nested, length(min = 1))]
650 #[eserde(compat)]
651 pub title: Option<Vec<Title>>,
652 #[validate(nested)]
654 #[eserde(compat)]
655 pub alternate_identifier: Option<Vec<AlternateIdentifier>>,
656 #[validate(nested)]
658 #[eserde(compat)]
659 pub alternate_url: Option<Vec<AlternateUrl>>,
660 #[validate(nested)]
662 #[eserde(compat)]
663 pub description: Option<Vec<Description>>,
664 #[validate(nested)]
666 #[eserde(compat)]
667 pub metadata: Option<MetadataMetadata>,
668 #[validate(nested)]
674 #[serde(alias = "organisation")]
675 #[eserde(compat)]
676 pub organization: Option<Vec<Organization>>,
677 #[validate(nested)]
679 #[eserde(compat)]
680 pub related_object: Option<Vec<RelatedObject>>,
681 #[validate(nested)]
683 #[eserde(compat)]
684 pub related_raid: Option<Vec<RelatedRaid>>,
685 #[validate(nested)]
687 #[eserde(compat)]
688 pub spatial_coverage: Option<Vec<SpatialCoverage>>,
689 #[validate(nested)]
691 #[eserde(compat)]
692 pub subject: Option<Vec<Subject>>,
693 #[validate(nested)]
695 #[eserde(compat)]
696 pub traditional_knowledge_label: Option<Vec<TraditionalKnowledgeLabel>>,
697}
698#[derive(Builder, Clone, Debug, Serialize, Deserialize, Display, JsonSchema, Validate)]
702#[display("{id:?}")]
703#[serde(deny_unknown_fields, rename_all = "camelCase")]
704pub struct MetadataIdentifier {
705 #[validate(required, custom(function = "is_raid"))]
709 pub id: Option<String>,
710 #[validate(required, url)]
714 pub schema_uri: Option<String>,
715 #[validate(required, nested)]
717 pub owner: Option<Owner>,
718 #[validate(required, url)]
720 pub raid_agency_url: Option<String>,
721 #[validate(required, nested)]
723 pub registration_agency: Option<RegistrationAgency>,
724 #[validate(required, nested)]
728 pub license: Option<License>,
729 #[validate(required, range(min = 0))]
731 pub version: Option<u32>,
732}
733#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
735#[serde(deny_unknown_fields, rename_all = "camelCase")]
736pub struct MetadataMetadata {
737 #[validate(custom(function = "is_unix_epoch"))]
741 pub created: usize,
742 #[validate(custom(function = "is_unix_epoch"))]
746 pub updated: usize,
747}
748#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
752#[serde(deny_unknown_fields, rename_all = "camelCase")]
753pub struct Organization {
754 #[validate(custom(function = "is_ror"))]
760 pub id: String,
761 #[validate(url, contains(pattern = "https://ror.org"))]
765 pub schema_uri: Option<String>,
766 #[validate(nested)]
768 pub role: Vec<OrganizationRole>,
769}
770#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
772#[serde(deny_unknown_fields, rename_all = "camelCase")]
773pub struct OrganizationRole {
774 pub id: OrganizationRoleType,
776 #[validate(url)]
778 pub schema_uri: Option<String>,
779 #[validate(custom(function = "has_start_date"), nested)]
781 #[serde(flatten)]
782 pub date: Date,
783}
784#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
788#[serde(deny_unknown_fields, rename_all = "camelCase")]
789pub struct Owner {
790 #[validate(custom(function = "is_ror"))]
796 pub id: String,
797 #[validate(url)]
801 pub schema_uri: Option<String>,
802 pub service_point: usize,
810}
811#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
813#[serde(deny_unknown_fields, rename_all = "camelCase")]
814pub struct Place {
815 pub text: Option<String>,
819 #[validate(nested)]
821 pub language: Option<Language>,
822}
823#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
825#[serde(deny_unknown_fields, rename_all = "camelCase")]
826pub struct RelatedObject {
827 pub id: String,
834 #[validate(url)]
836 pub schema_uri: Option<String>,
837 #[validate(nested)]
839 #[serde(rename = "type")]
840 pub related_object_type: RelatedObjectIdentifier,
841 #[validate(nested, length(min = 1))]
843 pub category: Vec<RelatedObjectCategory>,
844}
845#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
847#[serde(deny_unknown_fields, rename_all = "camelCase")]
848pub struct RelatedObjectCategory {
849 pub id: ObjectCategoryType,
851 #[validate(url)]
853 pub schema_uri: Option<String>,
854}
855#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
857#[serde(deny_unknown_fields, rename_all = "camelCase")]
858pub struct RelatedObjectIdentifier {
859 pub id: ObjectType,
861 #[validate(url)]
863 pub schema_uri: Option<String>,
864}
865#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
867#[serde(deny_unknown_fields, rename_all = "camelCase")]
868pub struct RelatedRaid {
869 pub id: String,
871 #[validate(nested)]
873 #[serde(rename = "type")]
874 pub related_raid_type: RelatedRaidIdentifier,
875}
876#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
878#[serde(deny_unknown_fields, rename_all = "camelCase")]
879pub struct RelatedRaidIdentifier {
880 pub id: RelatedRaidType,
882 #[validate(url)]
884 pub schema_uri: Option<String>,
885}
886#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
890#[serde(deny_unknown_fields, rename_all = "camelCase")]
891pub struct RegistrationAgency {
892 #[validate(custom(function = "is_ror"))]
896 pub id: String,
897 #[validate(url)]
901 pub schema_uri: Option<String>,
902}
903#[skip_serializing_none]
909#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
910#[builder(start_fn = init, on(String, into))]
911#[serde(deny_unknown_fields, rename_all = "camelCase")]
912pub struct Role {
913 #[validate(required)]
915 pub id: Option<CreditRole>,
916 #[builder(default = "https://credit.niso.org/".to_string())]
918 #[validate(url)]
919 pub schema_uri: String,
920}
921#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
925#[serde(deny_unknown_fields, rename_all = "camelCase")]
926pub struct SpatialCoverage {
927 pub id: String,
929 #[validate(url)]
931 pub schema_uri: Option<String>,
932 #[validate(nested)]
934 pub place: Vec<Place>,
935}
936#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
940#[serde(deny_unknown_fields, rename_all = "camelCase")]
941pub struct Subject {
942 pub id: String,
944 #[validate(url)]
946 pub schema_uri: Option<String>,
947 #[validate(required, nested)]
949 pub keyword: Option<Vec<Keyword>>,
950}
951#[skip_serializing_none]
955#[derive(Builder, Clone, Debug, Display, Serialize, Deserialize, JsonSchema, Validate)]
956#[builder(start_fn = init, on(String, into))]
957#[display("{text:?} ({title_type:?})")]
958#[serde(deny_unknown_fields, rename_all = "camelCase")]
959pub struct Title {
960 #[validate(required, length(min = 3, max = 100))]
962 pub text: Option<String>,
963 #[validate(required, nested)]
965 #[serde(rename = "type")]
966 pub title_type: Option<TitleIdentifier>,
967 #[validate(nested)]
969 pub language: Option<Language>,
970 #[validate(required, custom(function = "is_date"))]
976 pub start_date: Option<String>,
977 #[validate(custom(function = "is_year"))]
987 pub end_date: Option<String>,
988}
989#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
993#[serde(deny_unknown_fields, rename_all = "camelCase")]
994pub struct TraditionalKnowledgeLabel {
995 pub id: String,
999 #[validate(url)]
1003 pub schema_uri: Option<String>,
1004}
1005#[derive(Builder, Clone, Debug, Serialize, Deserialize, Display, JsonSchema, Validate)]
1007#[builder(start_fn = init, on(String, into))]
1008#[display("{id}")]
1009#[serde(deny_unknown_fields, rename_all = "camelCase")]
1010pub struct TitleIdentifier {
1011 pub id: TitleType,
1015 #[builder(default = "https://vocabulary.raid.org/title.type.schema/376".to_string())]
1017 #[validate(url)]
1018 pub schema_uri: String,
1019}
1020impl Metadata {
1021 #[cfg(feature = "std")]
1023 pub fn to_schema() {
1024 let schema = schema_for!(Metadata);
1025 match serde_json::to_string_pretty(&schema) {
1026 | Ok(json) => println!("{}", json),
1027 | Err(why) => eprintln!("Failed to serialize schema: {}", why),
1028 }
1029 }
1030 #[cfg(feature = "std")]
1032 pub fn read(path: PathBuf) -> Result<Metadata, Error> {
1033 match read_file(path) {
1034 | Ok(data) => match eserde::json::from_str::<Metadata>(&data) {
1035 | Ok(value) => Ok(value),
1036 | Err(errors) => {
1037 let details: Vec<String> = errors
1038 .iter()
1039 .map(|e| format!("{}: {}", e.path().map_or("root".into(), |p| p.to_string()), e.message()))
1040 .collect();
1041 Err(Error::other(details.join("\n")))
1042 }
1043 },
1044 | Err(why) => {
1045 let msg = format!("Read RAiD metadata file - {why}");
1046 error!("=> {} {}", Label::fail(), msg);
1047 Err(Error::other(msg))
1048 }
1049 }
1050 }
1051}
1052impl Contributor {
1053 fn is_contact(&self) -> bool {
1054 self.contact
1055 }
1056 fn is_leader(&self) -> bool {
1057 self.leader
1058 }
1059}
1060impl Organization {
1061 fn is_lead_research_organization(&self) -> bool {
1062 self.role
1063 .iter()
1064 .any(|role| matches!(role.id, OrganizationRoleType::LeadResearchOrganization))
1065 }
1066}
1067#[cfg(feature = "std")]
1068impl From<ResearchActivity> for Metadata {
1069 fn from(activity: ResearchActivity) -> Self {
1070 let default_start_date = current_date();
1071 let default_end_year = default_start_date.chars().take(4).collect::<String>();
1072 let raid_id = activity.meta.raid.as_ref().and_then(|raid| raid.first()).cloned().unwrap_or_default();
1073 let ror_id = activity.meta.ror.as_ref().and_then(|rors| rors.first()).cloned().unwrap_or_default();
1074 let contributor_id = activity.contact.identifier.clone().filter(|id| is_orcid(id).is_ok()).unwrap_or_default();
1075 let date = Date {
1076 start_date: Some(default_start_date.clone()),
1077 end_date: None,
1078 };
1079 let contributor_position = ContributorPosition::init()
1080 .id(PositionType::PrincipalInvestigator)
1081 .date(date.clone())
1082 .build();
1083 let contributor = Contributor {
1084 id: Some(contributor_id),
1085 schema_uri: Some(DEFAULT_ORCID_SCHEMA_URI.to_string()),
1086 status: Some("active".to_string()),
1087 status_message: None,
1088 position: vec![contributor_position],
1089 leader: true,
1090 contact: true,
1091 email: Some(activity.contact.email.clone()),
1092 role: None,
1093 uuid: None,
1094 };
1095 let org_role = OrganizationRole {
1096 id: OrganizationRoleType::LeadResearchOrganization,
1097 schema_uri: None,
1098 date: date.clone(),
1099 };
1100 let organization = Organization {
1101 id: ror_id.clone(),
1102 schema_uri: Some(DEFAULT_ROR_SCHEMA_URI.to_string()),
1103 role: vec![org_role],
1104 };
1105 let title = Title {
1106 text: Some(activity.title.clone()),
1107 title_type: Some(TitleIdentifier::init().id(TitleType::Primary).build()),
1108 language: None,
1109 start_date: Some(default_start_date.clone()),
1110 end_date: Some(default_end_year.clone()),
1111 };
1112 let description = Description {
1113 text: Some(activity.sections.mission.clone()),
1114 description_type: Some(DescriptionIdentifier {
1115 id: DescriptionType::Primary,
1116 schema_uri: "https://vocabulary.raid.org/description.type.schema/318".to_string(),
1117 }),
1118 language: None,
1119 };
1120 let identifier = MetadataIdentifier {
1121 id: Some(raid_id),
1122 schema_uri: Some("https://raid.org/".to_string()),
1123 owner: Some(Owner {
1124 id: ror_id.clone(),
1125 schema_uri: Some(DEFAULT_ROR_SCHEMA_URI.to_string()),
1126 service_point: 0,
1127 }),
1128 raid_agency_url: Some("https://raid.org/".to_string()),
1129 registration_agency: Some(RegistrationAgency {
1130 id: ror_id,
1131 schema_uri: Some(DEFAULT_ROR_SCHEMA_URI.to_string()),
1132 }),
1133 license: Some(License::Single("CC0-1.0".to_string())),
1134 version: Some(1),
1135 };
1136 let access = Access {
1137 access_type: Some(AccessIdentifier {
1138 id: AccessType::OpenAccess,
1139 schema_uri: "https://vocabularies.coar-repositories.org/access_rights/".to_string(),
1140 }),
1141 embargo_expiry: None,
1142 statement: None,
1143 };
1144 Metadata {
1145 access: Some(access),
1146 contributor: Some(vec![contributor]),
1147 date: None,
1148 description: Some(vec![description]),
1149 metadata: None,
1150 title: Some(vec![title]),
1151 identifier: Some(identifier),
1152 alternate_identifier: None,
1153 alternate_url: None,
1154 organization: Some(vec![organization]),
1155 related_object: None,
1156 related_raid: None,
1157 spatial_coverage: None,
1158 subject: None,
1159 traditional_knowledge_label: None,
1160 }
1161 }
1162}
1163fn has_contact(value: &Vec<Contributor>) -> Result<(), ValidationError> {
1164 has_at_least_one_truthy(
1165 value.as_slice(),
1166 Contributor::is_contact,
1167 "contributors",
1168 "Mark at least one contributor as contact",
1169 )
1170}
1171fn has_leader(value: &Vec<Contributor>) -> Result<(), ValidationError> {
1172 has_at_least_one_truthy(
1173 value.as_slice(),
1174 Contributor::is_leader,
1175 "contributors",
1176 "Mark at least one contributor as leader",
1177 )
1178}
1179fn has_start_date(value: &Date) -> Result<(), ValidationError> {
1180 match value.start_date.as_ref() {
1181 | Some(start_date) if !start_date.trim().is_empty() => Ok(()),
1182 | _ => Err(ValidationError::new("date").with_message("Provide valid start date".into())),
1183 }
1184}
1185fn validate_contributors(value: &Metadata) -> Result<(), ValidationError> {
1186 match &value.contributor {
1187 | Some(contributors) => {
1188 let message = [
1189 has_contact(contributors)
1190 .err()
1191 .map(|_| "Mark at least one contributor as contact".to_string()),
1192 has_leader(contributors)
1193 .err()
1194 .map(|_| "Mark at least one contributor as leader".to_string()),
1195 ]
1196 .into_iter()
1197 .flatten()
1198 .collect::<Vec<String>>()
1199 .join("; ");
1200 match message.is_empty() {
1201 | true => Ok(()),
1202 | false => Err(ValidationError::new("contributors").with_message(message.into())),
1203 }
1204 }
1205 | None => Ok(()),
1206 }
1207}
1208fn validate_organization(value: &Metadata) -> Result<(), ValidationError> {
1209 match &value.organization {
1210 | Some(organizations) => {
1211 let lead_count = organizations
1212 .iter()
1213 .filter(|organization| organization.is_lead_research_organization())
1214 .count();
1215 let message = if organizations.is_empty() {
1216 None
1217 } else if lead_count == 0 {
1218 Some("Mark one organization as lead research organization".to_string())
1219 } else if lead_count > 1 {
1220 Some("Only one organization can be lead research organization".to_string())
1221 } else {
1222 None
1223 };
1224 match message {
1225 | Some(value) => Err(ValidationError::new("organization").with_message(value.into())),
1226 | None => Ok(()),
1227 }
1228 }
1229 | None => Ok(()),
1230 }
1231}
1232fn validate_metadata(value: &Metadata) -> Result<(), ValidationError> {
1233 let message = [
1234 validate_contributors(value)
1235 .err()
1236 .map(|why| why.message.unwrap_or_else(|| "Invalid contributors".into()).to_string()),
1237 validate_organization(value)
1238 .err()
1239 .map(|why| why.message.unwrap_or_else(|| "Invalid organization".into()).to_string()),
1240 ]
1241 .into_iter()
1242 .flatten()
1243 .collect::<Vec<String>>()
1244 .join("; ");
1245 match message.is_empty() {
1246 | true => Ok(()),
1247 | false => Err(ValidationError::new("metadata").with_message(message.into())),
1248 }
1249}