Skip to main content

acorn/schema/pid/
raid.rs

1//! ## Research activity identifier (RAiD) metadata schema
2//!
3//! See <https://metadata.raid.org/en/v1.6/index.html> for official documentation on reference schema.
4//!
5//! Use ACORN to generate JSON schema for RAiD metadata with `acorn schema raid`
6#[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/// Allowed values for access types
35#[derive(Clone, Debug, Default, Deserialize, Display, JsonSchema, Serialize)]
36pub enum AccessType {
37    /// Open access
38    #[default]
39    #[display("open-access")]
40    #[serde(rename = "https://vocabularies.coar-repositories.org/access_rights/c_abf2/")]
41    OpenAccess,
42    /// Embargoed access
43    #[display("embargoed-access")]
44    #[serde(rename = "https://vocabularies.coar-repositories.org/access_rights/c_f1cf/")]
45    EmbargoedAccess,
46}
47/// CRediT role
48///
49/// Taxonomy of 14 roles that can be used to describe the key types of contributions typically made to the production and publication of research output such as research articles.
50///
51/// See <https://www.niso.org/publications/z39104-2022-credit>
52#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
53#[serde(rename_all = "kebab-case")]
54pub enum CreditRole {
55    /// Ideas; formulation or evolution of overarching research goals and aims.
56    #[display("conceptualization")]
57    #[serde(rename = "https://credit.niso.org/contributor-roles/conceptualization/")]
58    Conceptualization,
59    /// Management activities to annotate (produce metadata), scrub data and maintain research data (including software code, where it is necessary for interpreting the data itself) for initial use and later re-use.
60    #[display("data-curation")]
61    #[serde(rename = "https://credit.niso.org/contributor-roles/data-curation/")]
62    DataCuration,
63    /// Application of statistical, mathematical, computational, or other formal techniques to analyze or synthesize study data.
64    #[display("formal-analysis")]
65    #[serde(rename = "https://credit.niso.org/contributor-roles/formal-analysis/")]
66    FormalAnalysis,
67    /// Acquisition of the financial support for the project leading to this publication.
68    #[display("funding-acquisition")]
69    #[serde(rename = "https://credit.niso.org/contributor-roles/funding-acquisition/")]
70    FundingAcquisition,
71    /// Conducting a research and investigation process, specifically performing the experiments, or data/evidence collection.
72    #[display("investigation")]
73    #[serde(rename = "https://credit.niso.org/contributor-roles/investigation/")]
74    Investigation,
75    /// Development or design of methodology; creation of models.
76    #[display("methodology")]
77    #[serde(rename = "https://credit.niso.org/contributor-roles/methodology/")]
78    Methodology,
79    /// Management and coordination responsibility for the research activity planning and execution.
80    #[display("project-administration")]
81    #[serde(rename = "https://credit.niso.org/contributor-roles/project-administration/")]
82    ProjectAdministration,
83    /// Provision of study materials, reagents, materials, patients, laboratory samples, animals, instrumentation, computing resources, or other analysis tools.
84    #[display("resources")]
85    #[serde(rename = "https://credit.niso.org/contributor-roles/resources/")]
86    Resources,
87    /// Programming, software development; designing computer programs; implementation of the computer code and supporting algorithms; testing of existing code components.
88    #[display("software")]
89    #[serde(rename = "https://credit.niso.org/contributor-roles/software/")]
90    Software,
91    /// Oversight and leadership responsibility for the research activity planning and execution, including mentorship external to the core team.
92    #[display("supervision")]
93    #[serde(rename = "https://credit.niso.org/contributor-roles/supervision/")]
94    Supervision,
95    /// Verification, whether as a part of the activity or separate, of the overall replication/reproducibility of results/experiments and other research outputs.
96    #[display("validation")]
97    #[serde(rename = "https://credit.niso.org/contributor-roles/validation/")]
98    Validation,
99    /// Preparation, creation and/or presentation of the published work, specifically visualization/data presentation.
100    #[display("visualization")]
101    #[serde(rename = "https://credit.niso.org/contributor-roles/visualization/")]
102    Visualization,
103    /// Preparation, creation and/or presentation of the published work, specifically writing the initial draft (including substantive translation).
104    #[display("writing-original-draft")]
105    #[serde(rename = "https://credit.niso.org/contributor-roles/writing-original-draft/")]
106    WritingOriginalDraft,
107    /// Preparation, creation and/or presentation of the published work by those from the original research group, specifically critical review, commentary or revision - including pre- or post-publication stages
108    #[display("writing-review-editing")]
109    #[serde(rename = "https://credit.niso.org/contributor-roles/writing-review-editing/")]
110    WritingReviewEditing,
111}
112/// Description types
113#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
114#[serde(rename_all = "kebab-case")]
115pub enum DescriptionType {
116    /// Primary description (i.e., a preferred full description or abstract)
117    #[display("primary")]
118    #[serde(rename = "https://vocabulary.raid.org/description.type.schema/318")]
119    Primary,
120    /// An alternative description (i.e., an additional or supplementary full description or abstract)
121    #[display("alternative")]
122    #[serde(rename = "https://vocabulary.raid.org/description.type.schema/319")]
123    Alternative,
124    /// Brief description (i.e., a shorter version of the primary description)
125    #[display("brief")]
126    #[serde(rename = "https://vocabulary.raid.org/description.type.schema/3")]
127    Brief,
128    /// Significance statement
129    #[display("significance-statement")]
130    #[serde(rename = "https://vocabulary.raid.org/description.type.schema/9")]
131    SignificanceStatement,
132    /// Methods
133    #[display("methods")]
134    #[serde(rename = "https://vocabulary.raid.org/description.type.schema/8")]
135    Methods,
136    /// Objectives
137    #[display("objectives")]
138    #[serde(rename = "https://vocabulary.raid.org/description.type.schema/7")]
139    Objectives,
140    /// Acknowledgements (i.e., for recognition of people not listed as Contributors or organizations not listed as organizations)
141    #[display("acknowledgements")]
142    #[serde(rename = "https://vocabulary.raid.org/description.type.schema/392")]
143    Acknowledgements,
144    /// Other (i.e., any other descriptive information such as a note)
145    #[display("other")]
146    #[serde(rename = "https://vocabulary.raid.org/description.type.schema/6")]
147    Other,
148}
149/// Category of input, output, or process document
150#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
151#[serde(rename_all = "kebab-case")]
152pub enum ObjectCategoryType {
153    /// Output
154    #[display("output")]
155    #[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/190")]
156    Output,
157    /// Input
158    #[display("input")]
159    #[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/191")]
160    Input,
161    /// Internal process document or artifact
162    #[display("internal-process-document")]
163    #[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/192")]
164    InternalProcessDocument,
165}
166/// Type of input, output, or process document
167#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
168#[serde(rename_all = "kebab-case")]
169pub enum ObjectType {
170    /// Output management plan
171    #[display("output-management-plan")]
172    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/247")]
173    OutputManagementPlan,
174    /// Conference poster
175    #[display("conference-poster")]
176    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/248")]
177    ConferencePoster,
178    /// Workflow
179    #[display("workflow")]
180    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/249")]
181    Workflow,
182    /// Journal article
183    #[display("journal-article")]
184    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/250")]
185    JournalArticle,
186    /// Standard
187    #[display("standard")]
188    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/251")]
189    Standard,
190    /// Report
191    #[display("report")]
192    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/252")]
193    Report,
194    /// Dissertation
195    #[display("dissertation")]
196    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/253")]
197    Dissertation,
198    /// Preprint
199    #[display("preprint")]
200    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/254")]
201    Preprint,
202    /// Data paper
203    #[display("data-paper")]
204    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/255")]
205    DataPaper,
206    /// Computational notebook (e.g., Jupyter notebook)
207    #[display("computational-notebook")]
208    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/256")]
209    ComputationalNotebook,
210    /// Image
211    #[display("image")]
212    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/257")]
213    Image,
214    /// Book
215    #[display("book")]
216    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/258")]
217    Book,
218    /// Software
219    #[display("software")]
220    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/259")]
221    Software,
222    /// Event
223    #[display("event")]
224    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/260")]
225    Event,
226    /// Sound
227    #[display("sound")]
228    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/261")]
229    Sound,
230    /// Conference proceeding
231    #[display("conference-proceeding")]
232    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/262")]
233    ConferenceProceeding,
234    /// Model
235    #[display("model")]
236    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/263")]
237    Model,
238    /// Conference paper
239    #[display("conference-paper")]
240    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/264")]
241    ConferencePaper,
242    /// Text
243    #[display("text")]
244    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/265")]
245    Text,
246    /// Instrument
247    #[display("instrument")]
248    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/266")]
249    Instrument,
250    /// Learning object
251    #[display("learning-object")]
252    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/267")]
253    LearningObject,
254    /// Prize (excluding funded awards)
255    #[display("prize")]
256    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/268")]
257    Prize,
258    /// Dataset
259    #[display("dataset")]
260    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/269")]
261    Dataset,
262    /// Physical object
263    #[display("physical-object")]
264    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/270")]
265    PhysicalObject,
266    /// Book chapter
267    #[display("book-chapter")]
268    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/271")]
269    BookChapter,
270    /// Funding
271    /// ### Note
272    /// > Includes grants or other cash or in-kind awards, but not prizes
273    #[display("funding")]
274    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/272")]
275    Funding,
276    /// Audiovisual
277    #[display("audiovisual")]
278    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/273")]
279    Audiovisual,
280    /// Service
281    #[display("service")]
282    #[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/274")]
283    Service,
284}
285/// Organization role identifier
286#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
287#[serde(rename_all = "kebab-case")]
288pub enum OrganizationRoleType {
289    /// Lead research organization
290    #[display("lead-research-organization")]
291    #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/182")]
292    LeadResearchOrganization,
293    /// Other research organization
294    #[display("other-research-organization")]
295    #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/183")]
296    OtherResearchOrganization,
297    /// Partner organization (i.e., a non-research organization, such as an industry, government, or community partner that is collaborating on the project or activity, as a research partner rather than a hired consultant or contractor)
298    #[display("partner-organization")]
299    #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/184")]
300    PartnerOrganization,
301    /// Contractor (i.e., a consulting organization hired by the project)
302    #[display("contractor")]
303    #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/185")]
304    Contractor,
305    /// Funder (i.e., an organization underwriting the research via a cash or in-kind grant, prize, or investment, but not otherwise listed as a research organization, partner organization or contractor)
306    #[display("funder")]
307    #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/186")]
308    Funder,
309    /// Facility (i.e., an organization providing access to physical or digital infrastructure, but not otherwise listed as a research organization, partner organization or contractor)
310    #[display("facility")]
311    #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/187")]
312    Facility,
313    /// Other Organiation not covered by the roles above
314    #[display("other-organization")]
315    #[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/188")]
316    OtherOrganization,
317}
318/// Represents a contributor's administrative position on a project (such as their position on a grant application)
319///
320/// <div class="warning">Use contributor role to define scientific or scholarly contributions</div>
321#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
322#[serde(rename_all = "kebab-case")]
323pub enum PositionType {
324    /// Principal Investigator
325    #[display("principal-investigator")]
326    #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/307")]
327    PrincipalInvestigator,
328    /// Co-Investigator
329    #[display("co-investigator")]
330    #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/308")]
331    CoInvestigator,
332    /// Partner Investigator (e.g., industry, government, or community collaborator)
333    #[display("partner-investigator")]
334    #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/309")]
335    PartnerInvestigator,
336    /// Consultant (e.g., someone hired as a contract researcher by the project)
337    #[display("consultant")]
338    #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/310")]
339    Consultant,
340    /// Other Participant not covered by one of the positions above, e.g., "member" or "other significant contributor"
341    #[display("other")]
342    #[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/311")]
343    Other,
344}
345/// RAiD Relation Type
346///
347/// Describes the relationship being one activity and another
348#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
349#[serde(rename_all = "kebab-case")]
350pub enum RelatedRaidType {
351    /// Obsoletes
352    /// > For resolving duplicate RAiDs
353    #[display("obsoletes")]
354    #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/198")]
355    Obsoletes,
356    /// Is source of
357    #[display("is-source-of")]
358    #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/199")]
359    IsSourceOf,
360    /// Is derived from
361    #[display("is-derived-from")]
362    #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/200")]
363    IsDerivedFrom,
364    /// Has part
365    #[display("has-part")]
366    #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/201")]
367    HasPart,
368    /// Is part of
369    #[display("is-part-of")]
370    #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/202")]
371    IsPartOf,
372    /// Is continued by
373    #[display("is-continued-by")]
374    #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/203")]
375    IsContinuedBy,
376    /// Continues
377    #[display("continues")]
378    #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/204")]
379    Continues,
380    /// Is obsoleted by
381    /// > For resolving duplicate RAiDs
382    #[display("is-obsoleted-by")]
383    #[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/205")]
384    IsObsoletedBy,
385}
386/// Allowed values for title identifiers
387#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
388pub enum TitleType {
389    /// Title acronym
390    #[display("acronym")]
391    #[serde(rename = "https://vocabulary.raid.org/title.type.schema/156")]
392    Acronym,
393    /// Alternative title, including subtitle or other supplemental title
394    #[display("alternative")]
395    #[serde(rename = "https://vocabulary.raid.org/title.type.schema/4")]
396    Alternative,
397    /// Preferred full or long title
398    #[display("primary")]
399    #[serde(rename = "https://vocabulary.raid.org/title.type.schema/5")]
400    Primary,
401    /// Abreviated title
402    #[display("short")]
403    #[serde(rename = "https://vocabulary.raid.org/title.type.schema/157")]
404    Short,
405}
406/// Metadata schema block containing RAiD access information
407#[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    /// Access type
413    #[validate(required, nested)]
414    #[serde(rename = "type")]
415    pub access_type: Option<AccessIdentifier>,
416    /// Date an embargo on access to the RAiD metadata ends
417    /// ### Format
418    /// > [ISO 8601] standard date (e.g., `YYYY-MM-DD`)
419    ///
420    /// <div class="warning">Mandatory if access type is "embargoed"</div>
421    ///
422    /// <div class="warning">Embargo expiration dates may not lay more than 18 months from the date the RAiD was registered. Year, month, and day mush be specified.</div>
423    ///
424    /// [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601
425    #[validate(custom(function = "is_date"))]
426    pub embargo_expiry: Option<String>,
427    /// Access statement
428    ///
429    /// <div class="warning">Mandatory if access type is not "open"</div>
430    #[validate(nested)]
431    pub statement: Option<AccessStatement>,
432}
433/// Access type identifier
434#[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    /// Type of access granted to a RAiD metadata record
439    pub id: AccessType,
440    /// URI of the access type schema
441    #[builder(default = "https://vocabularies.coar-repositories.org/access_rights/".to_string())]
442    #[validate(url)]
443    pub schema_uri: String,
444}
445/// Metadata schema block containing an explanation for any access type that is not "open", with the explanation's associated properties
446#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
447#[serde(deny_unknown_fields, rename_all = "camelCase")]
448pub struct AccessStatement {
449    /// The text of an access statement that explains any restrictions on access
450    #[validate(length(min = 1, max = 1000))]
451    pub text: Option<String>,
452    /// The language of the access statement
453    #[validate(nested)]
454    pub language: Option<Language>,
455}
456/// Metadata schema block containing alternative local or global identifiers for the project or activity associated with the RAiD
457#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
458#[serde(deny_unknown_fields, rename_all = "camelCase")]
459pub struct AlternateIdentifier {
460    /// Identifier other than the RAiD applied to the project or activity
461    /// ### Example
462    /// > ACORN research activity data (RAD) [identifier]
463    ///
464    /// [identifier]: ./struct.Metadata.html#structfield.identifier
465    pub id: String,
466    /// Free text description of the type of alternate identifier supplied
467    #[serde(rename = "type")]
468    pub alternate_identifier_type: String,
469}
470/// Link to another website related to the project or activity
471#[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/// Metadata schema block containing a contributor to a RAiD and their associated properties
478///
479/// See <https://metadata.raid.org/en/v1.6/core/contributors.html>
480#[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    /// Contributor (person) associated with a project or activity identified by a persistent identifier (PID)
486    ///
487    /// Should be a valid *full* ORCiD
488    /// ### Example
489    /// > "<https://orcid.org/0000-0000-0000-0000>"
490    #[validate(required, custom(function = "is_orcid"))]
491    pub id: Option<String>,
492    /// URI of the contributor identifier schema
493    ///
494    /// <div class="warning">PID is required and (currently) only [ORCID] and [ISNI] are allowed</div>
495    ///
496    /// [ISNI]: https://isni.org/
497    /// [ORCID]: https://orcid.org/
498    #[validate(url)]
499    pub schema_uri: Option<String>,
500    /// Contibutor status
501    // TODO: Not in schema docs
502    pub status: Option<String>,
503    /// Text describing status
504    pub status_message: Option<String>,
505    /// Contributor's administrative position on a project or activity
506    // TODO: Schema docs list position as singular
507    #[validate(nested)]
508    pub position: Vec<ContributorPosition>,
509    /// Flag indicating that the contributor as a project leader
510    #[builder(default = false)]
511    pub leader: bool,
512    /// Flag indicating that the contributor as a project contact
513    #[builder(default = false)]
514    pub contact: bool,
515    /// Contributor email
516    // TODO: Not in schema docs
517    #[validate(email)]
518    pub email: Option<String>,
519    /// Contributor's role(s) on a project or activity
520    #[validate(nested)]
521    pub role: Option<Vec<Role>>,
522    /// Contributor UUID
523    // TODO: Not in schema docs
524    pub uuid: Option<String>,
525}
526/// Metadata schema sub-block describing a contributor's administrative position on a project or activity
527///
528/// See <https://metadata.raid.org/en/v1.6/core/contributors.html#contributor-position>
529#[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    /// Contributor's administrative position in the project
534    /// ### Example
535    /// > "Principal Investigator"
536    pub id: PositionType,
537    /// URI of the position schema used
538    ///
539    /// <div class="warning">Controlled list of schemas is informed by Simon Cox's [Project Ontology], [OpenAIRE] "Project" guidelines, NIH definitions, ARC definitions, and DataCite Metadata Schema 4.4 Appendix 1 Table 5 "Description of contributorType".</div>
540    ///
541    /// [OpenAIRE]: https://guidelines.openaire.eu/en/latest/
542    /// [Project Ontology]: http://linked.data.gov.au/def/project
543    #[builder(default = "https://vocabulary.raid.org/contributor.position.schema/305".to_string())]
544    #[validate(url)]
545    pub schema_uri: String,
546    /// Dates associated with contributor's involvement in a project or activity
547    #[validate(custom(function = "has_start_date"), nested)]
548    #[serde(flatten)]
549    pub date: Date,
550}
551/// Metadata schema block containing the description of the RAiD and associated properties
552///
553/// See <https://metadata.raid.org/en/v1.6/core/descriptions.html>
554#[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    /// Description text
560    #[validate(required, length(min = 3, max = 1000))]
561    pub text: Option<String>,
562    /// Description type information
563    #[validate(required, nested)]
564    #[serde(rename = "type")]
565    pub description_type: Option<DescriptionIdentifier>,
566    /// Language of the description text
567    #[validate(nested)]
568    pub language: Option<Language>,
569}
570/// Metadata schema block declaring the type of description
571#[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    /// Description identifier
576    pub id: DescriptionType,
577    /// URI of the associated description schema
578    #[builder(default = "https://vocabulary.raid.org/description.type.schema/320".to_string())]
579    #[validate(url)]
580    pub schema_uri: String,
581}
582/// Metadata schema block containing information about the associated type
583#[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    /// Type identifier
588    pub id: String,
589    /// URI of the associated type schema
590    #[validate(url)]
591    pub schema_uri: Option<String>,
592}
593/// Metadata schema sub-block containing free-text keyword describing a project plus associated properties
594#[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    /// Unconstrained keyword or key phrase describing the project or activity
599    pub text: String,
600    /// Language of the keyword
601    #[validate(nested)]
602    pub language: Option<Language>,
603}
604/// Metadata schema block declaring the language of the associated text
605#[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    /// Language used for the associated text, identified by a code or another identifier
610    /// ### Examples
611    /// - "eng"
612    /// - "fra"
613    /// - "jpn"
614    ///
615    /// <div class="warning">Limited to <a href="https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes">ISO 639:2023 (Set 3)</a></div>
616    #[validate(length(equal = 3))]
617    pub id: String,
618    /// URI of the associated type schema
619    #[validate(url)]
620    pub schema_uri: Option<String>,
621}
622/// Research Activity Identifier (RAiD) Metadata
623#[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    /// Access for the RAiD metadata
631    #[validate(required, nested)]
632    #[eserde(compat)]
633    pub access: Option<Access>,
634    /// Contributors to the RAiD
635    #[validate(required, nested, length(min = 1))]
636    #[eserde(compat)]
637    pub contributor: Option<Vec<Contributor>>,
638    /// Dates associated with the RAiD metadata
639    #[validate(required, custom(function = "has_start_date"), nested)]
640    #[eserde(compat)]
641    pub date: Option<Date>,
642    /// Metadata schema block containing the RAiD name and associated properties
643    #[validate(nested)]
644    #[eserde(compat)]
645    pub identifier: Option<MetadataIdentifier>,
646    /// Title metadata of the RAiD
647    ///
648    /// <div class="warning">One and only one title should be identified as "primary"</div>
649    #[validate(required, nested, length(min = 1))]
650    #[eserde(compat)]
651    pub title: Option<Vec<Title>>,
652    /// Alternate identifiers associated with the RAiD
653    #[validate(nested)]
654    #[eserde(compat)]
655    pub alternate_identifier: Option<Vec<AlternateIdentifier>>,
656    /// Alternate URLs associated with the RAiD
657    #[validate(nested)]
658    #[eserde(compat)]
659    pub alternate_url: Option<Vec<AlternateUrl>>,
660    /// Description metadata of the RAiD
661    #[validate(nested)]
662    #[eserde(compat)]
663    pub description: Option<Vec<Description>>,
664    /// RAiD metadata metadata
665    #[validate(nested)]
666    #[eserde(compat)]
667    pub metadata: Option<MetadataMetadata>,
668    /// Organizations associated with the RAiD
669    ///
670    /// <div class="warning">If only one organization is listed, it's role defaults to "Lead Research Organization"</div>
671    ///
672    /// <div class="warning">One and only one organization should be identified as "Lead Research Organization"</div>
673    #[validate(nested)]
674    #[serde(alias = "organisation")]
675    #[eserde(compat)]
676    pub organization: Option<Vec<Organization>>,
677    /// Related objects associated with the RAiD
678    #[validate(nested)]
679    #[eserde(compat)]
680    pub related_object: Option<Vec<RelatedObject>>,
681    /// Related RAiD(s) associated with the RAiD
682    #[validate(nested)]
683    #[eserde(compat)]
684    pub related_raid: Option<Vec<RelatedRaid>>,
685    /// Spatial coverage
686    #[validate(nested)]
687    #[eserde(compat)]
688    pub spatial_coverage: Option<Vec<SpatialCoverage>>,
689    /// Subjects
690    #[validate(nested)]
691    #[eserde(compat)]
692    pub subject: Option<Vec<Subject>>,
693    /// Traditional knowledge information
694    #[validate(nested)]
695    #[eserde(compat)]
696    pub traditional_knowledge_label: Option<Vec<TraditionalKnowledgeLabel>>,
697}
698/// Metadata schema block containing the RAiD name and associated properties
699///
700/// See <https://metadata.raid.org/en/v1.6/core/identifier.html#identifier>
701#[derive(Builder, Clone, Debug, Serialize, Deserialize, Display, JsonSchema, Validate)]
702#[display("{id:?}")]
703#[serde(deny_unknown_fields, rename_all = "camelCase")]
704pub struct MetadataIdentifier {
705    /// Unique alphanumeric character string that identifies a Research Activity Identifier (RAiD) name
706    /// ### Format
707    /// > `https://raid.org/prefix/suffix`
708    #[validate(required, custom(function = "is_raid"))]
709    pub id: Option<String>,
710    /// URI of the identifier scheme used to identify RAiDs
711    /// ### Example
712    /// > `https://raid.org/`
713    #[validate(required, url)]
714    pub schema_uri: Option<String>,
715    /// RAiD owner
716    #[validate(required, nested)]
717    pub owner: Option<Owner>,
718    /// RAiD agency URL
719    #[validate(required, url)]
720    pub raid_agency_url: Option<String>,
721    /// Mtadata schema sub-block declaring the Registration Agency that minted the RAiD
722    #[validate(required, nested)]
723    pub registration_agency: Option<RegistrationAgency>,
724    /// The licence, or licence waiver, under which the RAiD metadata record associated with this Identifier has been issued
725    ///
726    /// <div class="warning">Only supports CC-0 (?)</div>
727    #[validate(required, nested)]
728    pub license: Option<License>,
729    /// Version number of the RAiD
730    #[validate(required, range(min = 0))]
731    pub version: Option<u32>,
732}
733/// Information about edit history of associated RAiD
734#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
735#[serde(deny_unknown_fields, rename_all = "camelCase")]
736pub struct MetadataMetadata {
737    /// Date and time the RAiD metadata record was created
738    ///
739    /// Should be Unix epoch timestamp
740    #[validate(custom(function = "is_unix_epoch"))]
741    pub created: usize,
742    /// Date and time the RAiD metadata record was last updated
743    ///
744    /// Should be Unix epoch timestamp
745    #[validate(custom(function = "is_unix_epoch"))]
746    pub updated: usize,
747}
748/// Metadata schema block containing the organization associated with a RAiD and its associated properties
749///
750/// See <https://metadata.raid.org/en/v1.6/core/organisations.html#organisation>
751#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
752#[serde(deny_unknown_fields, rename_all = "camelCase")]
753pub struct Organization {
754    /// Organization identifier
755    /// ### Example
756    /// > `https://ror.org/01qz5mb56`
757    ///
758    /// <div class="warning">Should be <a href="https://ror.org">ROR</a>, if available</div>
759    #[validate(custom(function = "is_ror"))]
760    pub id: String,
761    /// URI of the organization identifier schema
762    ///
763    /// Only allowed value: `https://ror.org/`
764    #[validate(url, contains(pattern = "https://ror.org"))]
765    pub schema_uri: Option<String>,
766    /// Organization role
767    #[validate(nested)]
768    pub role: Vec<OrganizationRole>,
769}
770/// Organization role
771#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
772#[serde(deny_unknown_fields, rename_all = "camelCase")]
773pub struct OrganizationRole {
774    /// Organization role identifier
775    pub id: OrganizationRoleType,
776    /// URI of the organization role identifier schema
777    #[validate(url)]
778    pub schema_uri: Option<String>,
779    /// Date information associated with the organization role
780    #[validate(custom(function = "has_start_date"), nested)]
781    #[serde(flatten)]
782    pub date: Date,
783}
784/// Metadata schema sub-block that declares the owner of the RAiD (i.e. the organization requesting the RAiD)
785///
786/// See <https://metadata.raid.org/en/v1.6/core/identifier.html#identifier-owner>
787#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
788#[serde(deny_unknown_fields, rename_all = "camelCase")]
789pub struct Owner {
790    /// Persistent identifier of the legal entity responsible for the RAiD
791    ///
792    /// *Default* ROR of the organization requesting the RAiD
793    /// ### Example
794    /// > `https://ror.org/01qz5mb56` (ORNL)
795    #[validate(custom(function = "is_ror"))]
796    pub id: String,
797    /// URI of the identifier scheme used to identify RAiDs
798    /// ### Example
799    /// > `https://ror.org/`
800    #[validate(url)]
801    pub schema_uri: Option<String>,
802    /// Service point (SP) that requested the RAiD
803    /// ### Example
804    /// > `20000003`
805    /// ### Notes
806    /// - RAiD owners can have multiple SPs
807    /// - SPs do not need to be legal entities
808    /// - List of SPs is maintained by each [`RegistrationAgency`]
809    pub service_point: usize,
810}
811/// Metadata schema sub-block containing free-text place names or descriptions plus associated metadata properties
812#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
813#[serde(deny_unknown_fields, rename_all = "camelCase")]
814pub struct Place {
815    /// Free text description of one or more geographic locations that are the subject or target of the project or activity; use to specify or describe a geographic location in a manner not covered by [`SpatialCoverage`].id
816    /// ### Warning
817    /// > Do not duplicate information from [`SpatialCoverage`].id above; do not use for organisational locations (which are derived from the organisation's ROR)
818    pub text: Option<String>,
819    /// Language of the text
820    #[validate(nested)]
821    pub language: Option<Language>,
822}
823/// Metadata schema block containing inputs, outputs, and process documents related to a RAiD plus associated properties
824#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
825#[serde(deny_unknown_fields, rename_all = "camelCase")]
826pub struct RelatedObject {
827    /// Persistent identifier (PID) of related object
828    ///
829    /// The object can be any combination of
830    /// - input or resource used by a project or activity
831    /// - output or product created by a project or activity
832    /// - internal process documentation used within a project or activity
833    pub id: String,
834    /// URI of the relatedObject identifier schema
835    #[validate(url)]
836    pub schema_uri: Option<String>,
837    /// Type information of related object
838    #[validate(nested)]
839    #[serde(rename = "type")]
840    pub related_object_type: RelatedObjectIdentifier,
841    /// Category information of related object
842    #[validate(nested, length(min = 1))]
843    pub category: Vec<RelatedObjectCategory>,
844}
845/// Related object category information
846#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
847#[serde(deny_unknown_fields, rename_all = "camelCase")]
848pub struct RelatedObjectCategory {
849    /// Related object category identifier
850    pub id: ObjectCategoryType,
851    /// URI of the category schema used
852    #[validate(url)]
853    pub schema_uri: Option<String>,
854}
855/// Related object identifier
856#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
857#[serde(deny_unknown_fields, rename_all = "camelCase")]
858pub struct RelatedObjectIdentifier {
859    /// Related object type identifier
860    pub id: ObjectType,
861    /// URI of the related object type identifier schema
862    #[validate(url)]
863    pub schema_uri: Option<String>,
864}
865/// Metadata schema block containing related RAiDs and qualifying the relationship
866#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
867#[serde(deny_unknown_fields, rename_all = "camelCase")]
868pub struct RelatedRaid {
869    /// Subsidiary or otherwise related RAiD
870    pub id: String,
871    /// Related RAiD type
872    #[validate(nested)]
873    #[serde(rename = "type")]
874    pub related_raid_type: RelatedRaidIdentifier,
875}
876/// Related RAiD identifier
877#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
878#[serde(deny_unknown_fields, rename_all = "camelCase")]
879pub struct RelatedRaidIdentifier {
880    /// Related RAiD type identifier
881    pub id: RelatedRaidType,
882    /// URI of the related RAiD type identifier schema
883    #[validate(url)]
884    pub schema_uri: Option<String>,
885}
886/// Metadata schema block containing the RAiD name and associated properties
887///
888/// See <https://metadata.raid.org/en/v1.6/core/identifier.html#identifier-registrationagency>
889#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
890#[serde(deny_unknown_fields, rename_all = "camelCase")]
891pub struct RegistrationAgency {
892    /// Persistent identifier of the RAiD Registration Agency that minted the RAiD
893    ///
894    /// *Default* ROR of the RAiD Registration Agency
895    #[validate(custom(function = "is_ror"))]
896    pub id: String,
897    /// URI of the identifier scheme used to identify RAiDs
898    /// ### Example
899    /// > `https://raid.org/`
900    #[validate(url)]
901    pub schema_uri: Option<String>,
902}
903/// Metadata schema sub-block describing a contributor's scientific or scholarly role on a project using the [CRediT] vocabulary
904///
905/// See <https://metadata.raid.org/en/v1.6/core/contributors.html#contributor-role>
906///
907/// [CRediT]: https://credit.niso.org/
908#[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    /// Contributor role on a project or activity
914    #[validate(required)]
915    pub id: Option<CreditRole>,
916    /// URI of the role schema used
917    #[builder(default = "https://credit.niso.org/".to_string())]
918    #[validate(url)]
919    pub schema_uri: String,
920}
921/// Metadata schema block containing information about any spatial region(s) or named place(s) targeted by the project
922/// ### Note
923/// > Part of "extended" metadata that allows some customization by Registration Agencies
924#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
925#[serde(deny_unknown_fields, rename_all = "camelCase")]
926pub struct SpatialCoverage {
927    /// Spatial region or named place that is the subject or target of the project or activity. Repeat this property as necessary to indicate different locations. Do not duplicate organisational locations
928    pub id: String,
929    /// URI of the geolocation schema used for spatial coverage
930    #[validate(url)]
931    pub schema_uri: Option<String>,
932    /// Places of associated spatial coverage
933    #[validate(nested)]
934    pub place: Vec<Place>,
935}
936/// Metadata schema block containing the subject area of the RAiD plus associated properties
937/// ### Note
938/// > Part of "extended" metadata that allows some customization by Registration Agencies
939#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
940#[serde(deny_unknown_fields, rename_all = "camelCase")]
941pub struct Subject {
942    /// URI for a subject area or classification code describing the project or activity
943    pub id: String,
944    /// URI of the subject identifier schema
945    #[validate(url)]
946    pub schema_uri: Option<String>,
947    /// Subject keywords
948    #[validate(required, nested)]
949    pub keyword: Option<Vec<Keyword>>,
950}
951/// Metadata schema block containing the title of RAiD and associated properties
952///
953/// See <https://metadata.raid.org/en/v1.6/core/titles.html>
954#[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    /// Name or title by which the project or activity is known
961    #[validate(required, length(min = 3, max = 100))]
962    pub text: Option<String>,
963    /// Metadata schema block containing information about the title type
964    #[validate(required, nested)]
965    #[serde(rename = "type")]
966    pub title_type: Option<TitleIdentifier>,
967    /// Language of the title
968    #[validate(nested)]
969    pub language: Option<Language>,
970    /// Date the project or activity's title began being used
971    /// ### Format
972    /// > [ISO 8601] standard date (e.g., `YYYY-MM-DD`)
973    ///
974    /// [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601
975    #[validate(required, custom(function = "is_date"))]
976    pub start_date: Option<String>,
977    /// Date the project or activity title was changed or stopped being used
978    /// ### Format
979    /// > [ISO 8601] standard date (e.g., `YYYY-MM-DD`)
980    ///
981    /// <div class="warning">Only the year is required, month and day are optional</div>
982    ///
983    /// <div class="warning">Listed as "recommended" (optional) and "required"</div>
984    ///
985    /// [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601
986    #[validate(custom(function = "is_year"))]
987    pub end_date: Option<String>,
988}
989/// Metadata schema block containing information about Traditional Knowledge / Biocultural Labels and Notices
990/// ### Note
991/// > Part of "extended" metadata that allows some customization by Registration Agencies
992#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
993#[serde(deny_unknown_fields, rename_all = "camelCase")]
994pub struct TraditionalKnowledgeLabel {
995    /// Identifier (URI) linking to a verified source for Traditional Knowledge (TK) or Biocultural (BC) Labels or Notices pertaining to a project or activity
996    /// ### Note
997    /// > Currently only Local Contexts Hub Projects are allowed as a source for validated TK/BC Labels and Notices.
998    pub id: String,
999    /// URI of the Traditional Knowledge or Biocultural label identifier schema
1000    /// ### Note
1001    /// > Currently only Local Contexts Hub is supported for validated TK/BC Labels and Notices.
1002    #[validate(url)]
1003    pub schema_uri: Option<String>,
1004}
1005/// Metadata schema block containing information about the title type
1006#[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    /// Title type
1012    ///
1013    /// <div class="warning">Only one title should be identified as "Primary"</div>
1014    pub id: TitleType,
1015    /// URI of the title type schema
1016    #[builder(default = "https://vocabulary.raid.org/title.type.schema/376".to_string())]
1017    #[validate(url)]
1018    pub schema_uri: String,
1019}
1020impl Metadata {
1021    /// Print research activity identifier (RAiD) metadata schema as JSON schema
1022    #[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    /// Read RAiD metadata from a file
1031    #[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}