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