use crate::pid::{PersistentIdentifier, PersistentIdentifierConvert, PersistentIdentifierParse};
use crate::util::constants::{RE_DOI, RE_RAID_TEXT};
use crate::validation::{report_of, validate_license, IntoValidationReport, Validate, ValidationReport};
use crate::vocabulary;
use crate::Date;
use crate::OneOrMany;
use acorn_core::prelude::alloc::{format, String, ToString, Vec};
use acorn_core::util::{regex_capture_lookup, trim_unmatched_trailing_parentheses, StringExt};
use acorn_core::validation::ValidationError;
use alloc::collections::BTreeSet;
use bon::Builder;
use core::fmt;
use derive_more::Display;
#[cfg(feature = "std")]
use schemars::schema_for;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use strum::EnumIs;
#[derive(Clone, Debug, Default, Deserialize, Display, JsonSchema, Serialize)]
pub enum AccessType {
#[default]
#[display("open-access")]
#[serde(rename = "https://vocabularies.coar-repositories.org/access_rights/c_abf2/")]
OpenAccess,
#[display("embargoed-access")]
#[serde(rename = "https://vocabularies.coar-repositories.org/access_rights/c_f1cf/")]
EmbargoedAccess,
}
#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum CreditRole {
#[display("conceptualization")]
#[serde(rename = "https://credit.niso.org/contributor-roles/conceptualization/")]
Conceptualization,
#[display("data-curation")]
#[serde(rename = "https://credit.niso.org/contributor-roles/data-curation/")]
DataCuration,
#[display("formal-analysis")]
#[serde(rename = "https://credit.niso.org/contributor-roles/formal-analysis/")]
FormalAnalysis,
#[display("funding-acquisition")]
#[serde(rename = "https://credit.niso.org/contributor-roles/funding-acquisition/")]
FundingAcquisition,
#[display("investigation")]
#[serde(rename = "https://credit.niso.org/contributor-roles/investigation/")]
Investigation,
#[display("methodology")]
#[serde(rename = "https://credit.niso.org/contributor-roles/methodology/")]
Methodology,
#[display("project-administration")]
#[serde(rename = "https://credit.niso.org/contributor-roles/project-administration/")]
ProjectAdministration,
#[display("resources")]
#[serde(rename = "https://credit.niso.org/contributor-roles/resources/")]
Resources,
#[display("software")]
#[serde(rename = "https://credit.niso.org/contributor-roles/software/")]
Software,
#[display("supervision")]
#[serde(rename = "https://credit.niso.org/contributor-roles/supervision/")]
Supervision,
#[display("validation")]
#[serde(rename = "https://credit.niso.org/contributor-roles/validation/")]
Validation,
#[display("visualization")]
#[serde(rename = "https://credit.niso.org/contributor-roles/visualization/")]
Visualization,
#[display("writing-original-draft")]
#[serde(rename = "https://credit.niso.org/contributor-roles/writing-original-draft/")]
WritingOriginalDraft,
#[display("writing-review-editing")]
#[serde(rename = "https://credit.niso.org/contributor-roles/writing-review-editing/")]
WritingReviewEditing,
}
#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DescriptionType {
#[display("primary")]
#[serde(rename = "https://vocabulary.raid.org/description.type.schema/318")]
Primary,
#[display("alternative")]
#[serde(rename = "https://vocabulary.raid.org/description.type.schema/319")]
Alternative,
#[display("brief")]
#[serde(rename = "https://vocabulary.raid.org/description.type.schema/3")]
Brief,
#[display("significance-statement")]
#[serde(rename = "https://vocabulary.raid.org/description.type.schema/9")]
SignificanceStatement,
#[display("methods")]
#[serde(rename = "https://vocabulary.raid.org/description.type.schema/8")]
Methods,
#[display("objectives")]
#[serde(rename = "https://vocabulary.raid.org/description.type.schema/7")]
Objectives,
#[display("acknowledgements")]
#[serde(rename = "https://vocabulary.raid.org/description.type.schema/392")]
Acknowledgements,
#[display("other")]
#[serde(rename = "https://vocabulary.raid.org/description.type.schema/6")]
Other,
}
#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ObjectCategoryType {
#[display("output")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/190")]
Output,
#[display("input")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/191")]
Input,
#[display("internal-process-document")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.category.id/192")]
InternalProcessDocument,
}
#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ObjectType {
#[display("output-management-plan")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/247")]
OutputManagementPlan,
#[display("conference-poster")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/248")]
ConferencePoster,
#[display("workflow")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/249")]
Workflow,
#[display("journal-article")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/250")]
JournalArticle,
#[display("standard")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/251")]
Standard,
#[display("report")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/252")]
Report,
#[display("dissertation")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/253")]
Dissertation,
#[display("preprint")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/254")]
Preprint,
#[display("data-paper")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/255")]
DataPaper,
#[display("computational-notebook")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/256")]
ComputationalNotebook,
#[display("image")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/257")]
Image,
#[display("book")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/258")]
Book,
#[display("software")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/259")]
Software,
#[display("event")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/260")]
Event,
#[display("sound")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/261")]
Sound,
#[display("conference-proceeding")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/262")]
ConferenceProceeding,
#[display("model")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/263")]
Model,
#[display("conference-paper")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/264")]
ConferencePaper,
#[display("text")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/265")]
Text,
#[display("instrument")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/266")]
Instrument,
#[display("learning-object")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/267")]
LearningObject,
#[display("prize")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/268")]
Prize,
#[display("dataset")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/269")]
Dataset,
#[display("physical-object")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/270")]
PhysicalObject,
#[display("book-chapter")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/271")]
BookChapter,
#[display("funding")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/272")]
Funding,
#[display("audiovisual")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/273")]
Audiovisual,
#[display("service")]
#[serde(rename = "https://vocabulary.raid.org/relatedObject.type.schema/274")]
Service,
}
#[derive(Clone, Debug, Deserialize, Display, EnumIs, JsonSchema, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum OrganizationRoleType {
#[display("lead-research-organization")]
#[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/182")]
LeadResearchOrganization,
#[display("other-research-organization")]
#[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/183")]
OtherResearchOrganization,
#[display("partner-organization")]
#[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/184")]
PartnerOrganization,
#[display("contractor")]
#[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/185")]
Contractor,
#[display("funder")]
#[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/186")]
Funder,
#[display("facility")]
#[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/187")]
Facility,
#[display("other-organization")]
#[serde(rename = "https://vocabulary.raid.org/organisation.role.schema/188")]
OtherOrganization,
}
#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum PositionType {
#[display("principal-investigator")]
#[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/307")]
PrincipalInvestigator,
#[display("co-investigator")]
#[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/308")]
CoInvestigator,
#[display("partner-investigator")]
#[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/309")]
PartnerInvestigator,
#[display("consultant")]
#[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/310")]
Consultant,
#[display("other")]
#[serde(rename = "https://vocabulary.raid.org/contributor.position.schema/311")]
Other,
}
#[derive(Clone, Debug, Deserialize, Display, JsonSchema, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RelatedRaidType {
#[display("obsoletes")]
#[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/198")]
Obsoletes,
#[display("is-source-of")]
#[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/199")]
IsSourceOf,
#[display("is-derived-from")]
#[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/200")]
IsDerivedFrom,
#[display("has-part")]
#[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/201")]
HasPart,
#[display("is-part-of")]
#[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/202")]
IsPartOf,
#[display("is-continued-by")]
#[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/203")]
IsContinuedBy,
#[display("continues")]
#[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/204")]
Continues,
#[display("is-obsoleted-by")]
#[serde(rename = "https://vocabulary.raid.org/relatedRaid.type.schema/205")]
IsObsoletedBy,
}
#[derive(Clone, Debug, Deserialize, Display, EnumIs, JsonSchema, Serialize)]
pub enum TitleType {
#[display("acronym")]
#[serde(rename = "https://vocabulary.raid.org/title.type.schema/156")]
Acronym,
#[display("alternative")]
#[serde(rename = "https://vocabulary.raid.org/title.type.schema/4")]
Alternative,
#[display("primary")]
#[serde(rename = "https://vocabulary.raid.org/title.type.schema/5")]
Primary,
#[display("short")]
#[serde(rename = "https://vocabulary.raid.org/title.type.schema/157")]
Short,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
#[builder(start_fn = init)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Access {
#[validate(required, nested)]
#[serde(rename = "type")]
pub access_type: Option<AccessIdentifier>,
#[validate(date)]
pub embargo_expiry: Option<String>,
#[validate(nested)]
pub statement: Option<AccessStatement>,
}
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct AccessIdentifier {
pub id: AccessType,
#[builder(default = "https://vocabularies.coar-repositories.org/access_rights/".to_string())]
#[validate(url)]
pub schema_uri: String,
}
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct AccessStatement {
#[validate(length(min = 1, max = 1000))]
pub text: Option<String>,
#[validate(nested)]
pub language: Option<Language>,
}
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct AlternateIdentifier {
pub id: String,
#[serde(rename = "type")]
pub alternate_identifier_type: String,
}
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct AlternateUrl {
#[validate(url)]
url: String,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
#[builder(start_fn = init, on(String, into))]
#[validate(schema(function = "validate_contributor_identifier", skip_on_field_errors = false))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Contributor {
#[validate(required)]
pub id: Option<String>,
#[validate(required, url)]
pub schema_uri: Option<String>,
pub status: Option<String>,
pub status_message: Option<String>,
#[validate(nested)]
pub position: Vec<ContributorPosition>,
#[builder(default = false)]
pub leader: bool,
#[builder(default = false)]
pub contact: bool,
#[validate(email)]
pub email: Option<String>,
#[validate(nested)]
pub role: Option<Vec<Role>>,
pub uuid: Option<String>,
}
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ContributorPosition {
pub id: PositionType,
#[builder(default = "https://vocabulary.raid.org/contributor.position.schema/305".to_string())]
#[validate(url)]
pub schema_uri: String,
#[validate(custom(function = "has_start_date"), nested)]
#[serde(flatten)]
pub date: Date,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Description {
#[validate(required, length(min = 3, max = 1000))]
pub text: Option<String>,
#[validate(required, nested)]
#[serde(rename = "type")]
pub description_type: Option<DescriptionIdentifier>,
#[validate(nested)]
pub language: Option<Language>,
}
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct DescriptionIdentifier {
pub id: DescriptionType,
#[builder(default = "https://vocabulary.raid.org/description.type.schema/320".to_string())]
#[validate(url)]
pub schema_uri: String,
}
pub struct Extractor<'a> {
records: &'a [Metadata],
}
#[derive(Builder, Clone, Debug, Deserialize, Serialize, JsonSchema, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Identifier {
pub id: String,
#[validate(url)]
pub schema_uri: Option<String>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Keyword {
pub text: String,
#[validate(nested)]
pub language: Option<Language>,
}
#[derive(Builder, Clone, Debug, Deserialize, Serialize, JsonSchema, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Language {
#[validate(length(equal = 3))]
pub id: String,
#[validate(url)]
pub schema_uri: Option<String>,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Display, eserde::Deserialize, Serialize, JsonSchema, Validate)]
#[builder(start_fn = init)]
#[display("({identifier:?})")]
#[validate(schema(function = "validate_metadata", skip_on_field_errors = false))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Metadata {
#[validate(required, nested)]
#[eserde(compat)]
pub access: Option<Access>,
#[validate(required, nested, length(min = 1))]
#[eserde(compat)]
pub contributor: Option<Vec<Contributor>>,
#[validate(required, custom(function = "has_start_date"), nested)]
#[eserde(compat)]
pub date: Option<Date>,
#[validate(nested)]
#[eserde(compat)]
pub identifier: Option<MetadataIdentifier>,
#[validate(required, nested, length(min = 1))]
#[eserde(compat)]
pub title: Option<Vec<Title>>,
#[validate(nested)]
#[eserde(compat)]
pub alternate_identifier: Option<Vec<AlternateIdentifier>>,
#[validate(nested)]
#[eserde(compat)]
pub alternate_url: Option<Vec<AlternateUrl>>,
#[validate(nested)]
#[eserde(compat)]
pub description: Option<Vec<Description>>,
#[validate(nested)]
#[eserde(compat)]
pub metadata: Option<MetadataMetadata>,
#[validate(nested)]
#[serde(alias = "organisation")]
#[eserde(compat)]
pub organization: Option<Vec<Organization>>,
#[validate(nested)]
#[eserde(compat)]
pub related_object: Option<Vec<RelatedObject>>,
#[validate(nested)]
#[eserde(compat)]
pub related_raid: Option<Vec<RelatedRaid>>,
#[validate(nested)]
#[eserde(compat)]
pub spatial_coverage: Option<Vec<SpatialCoverage>>,
#[validate(nested)]
#[eserde(compat)]
pub subject: Option<Vec<Subject>>,
#[validate(nested)]
#[eserde(compat)]
pub traditional_knowledge_label: Option<Vec<TraditionalKnowledgeLabel>>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, Display, JsonSchema, Validate)]
#[display("{id:?}")]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct MetadataIdentifier {
#[validate(required, raid)]
pub id: Option<String>,
#[validate(required, url)]
pub schema_uri: Option<String>,
#[validate(required, nested)]
pub owner: Option<Owner>,
#[validate(required, url)]
pub raid_agency_url: Option<String>,
#[validate(required, nested)]
pub registration_agency: Option<RegistrationAgency>,
#[validate(required, custom(function = "validate_license"))]
pub license: Option<OneOrMany<String>>,
#[validate(required, range(min = 0))]
pub version: Option<u32>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct MetadataMetadata {
#[validate(unix_epoch)]
pub created: usize,
#[validate(unix_epoch)]
pub updated: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Organization {
#[validate(ror)]
pub id: String,
#[validate(url, contains(pattern = "https://ror.org"))]
pub schema_uri: Option<String>,
#[validate(nested)]
pub role: Vec<OrganizationRole>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct OrganizationRole {
pub id: OrganizationRoleType,
#[validate(url)]
pub schema_uri: Option<String>,
#[validate(custom(function = "has_start_date"), nested)]
#[serde(flatten)]
pub date: Date,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Owner {
#[validate(ror)]
pub id: String,
#[validate(url)]
pub schema_uri: Option<String>,
pub service_point: usize,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Place {
pub text: Option<String>,
#[validate(nested)]
pub language: Option<Language>,
}
#[derive(Builder, Clone, Debug)]
#[builder(start_fn = init, on(String, into))]
pub struct RAID {
pub schema_uri: Option<String>,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub metadata: Option<Metadata>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RegistrationAgency {
#[validate(ror)]
pub id: String,
#[validate(url)]
pub schema_uri: Option<String>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RelatedObject {
pub id: String,
#[validate(url)]
pub schema_uri: Option<String>,
#[validate(nested)]
#[serde(rename = "type")]
pub related_object_type: RelatedObjectIdentifier,
#[validate(nested, length(min = 1))]
pub category: Vec<RelatedObjectCategory>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RelatedObjectCategory {
pub id: ObjectCategoryType,
#[validate(url)]
pub schema_uri: Option<String>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RelatedObjectIdentifier {
pub id: ObjectType,
#[validate(url)]
pub schema_uri: Option<String>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RelatedRaid {
pub id: String,
#[validate(nested)]
#[serde(rename = "type")]
pub related_raid_type: RelatedRaidIdentifier,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RelatedRaidIdentifier {
pub id: RelatedRaidType,
#[validate(url)]
pub schema_uri: Option<String>,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Role {
#[validate(required)]
pub id: Option<CreditRole>,
#[builder(default = "https://credit.niso.org/".to_string())]
#[validate(url)]
pub schema_uri: String,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct SpatialCoverage {
pub id: String,
#[validate(url)]
pub schema_uri: Option<String>,
#[validate(nested)]
pub place: Vec<Place>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Subject {
pub id: String,
#[validate(url)]
pub schema_uri: Option<String>,
#[validate(required, nested)]
pub keyword: Option<Vec<Keyword>>,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Display, Serialize, Deserialize, JsonSchema, Validate)]
#[builder(start_fn = init, on(String, into))]
#[display("{text:?} ({title_type:?})")]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Title {
#[validate(required, length(min = 3, max = 100))]
pub text: Option<String>,
#[validate(required, nested)]
#[serde(rename = "type")]
pub title_type: Option<TitleIdentifier>,
#[validate(nested)]
pub language: Option<Language>,
#[validate(required, date)]
pub start_date: Option<String>,
#[validate(year)]
pub end_date: Option<String>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, Display, JsonSchema, Validate)]
#[builder(start_fn = init, on(String, into))]
#[display("{id}")]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct TitleIdentifier {
pub id: TitleType,
#[builder(default = "https://vocabulary.raid.org/title.type.schema/376".to_string())]
#[validate(url)]
pub schema_uri: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct TraditionalKnowledgeLabel {
pub id: String,
#[validate(url)]
pub schema_uri: Option<String>,
}
impl AlternateUrl {
pub fn url(&self) -> &str {
&self.url
}
}
impl Contributor {
fn is_contact(&self) -> bool {
self.contact
}
fn is_leader(&self) -> bool {
self.leader
}
}
impl TryFrom<&str> for CreditRole {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value
.trim()
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or_default()
.to_ascii_lowercase()
.as_str()
{
| "conceptualization" => Ok(Self::Conceptualization),
| "data-curation" => Ok(Self::DataCuration),
| "formal-analysis" => Ok(Self::FormalAnalysis),
| "funding-acquisition" => Ok(Self::FundingAcquisition),
| "investigation" => Ok(Self::Investigation),
| "methodology" => Ok(Self::Methodology),
| "project-administration" => Ok(Self::ProjectAdministration),
| "resources" => Ok(Self::Resources),
| "software" => Ok(Self::Software),
| "supervision" => Ok(Self::Supervision),
| "validation" => Ok(Self::Validation),
| "visualization" => Ok(Self::Visualization),
| "writing-original-draft" => Ok(Self::WritingOriginalDraft),
| "writing-review-editing" => Ok(Self::WritingReviewEditing),
| _ => Err("unknown CRediT role"),
}
}
}
impl<'a> Extractor<'a> {
pub fn new(records: &'a [Metadata]) -> Self {
Self { records }
}
fn titles(&self) -> impl Iterator<Item = &Title> {
self.records.iter().flat_map(|record| record.title.iter().flatten())
}
pub fn title(&self) -> Option<String> {
self.titles()
.filter(|title| title.title_type.as_ref().is_some_and(|kind| kind.id.is_primary()))
.find_map(|title| title.text.as_ref().filter(|value| !value.trim().is_empty()))
.cloned()
.or_else(|| {
self.titles()
.find_map(|title| title.text.as_ref().filter(|value| !value.trim().is_empty()))
.cloned()
})
}
pub fn contact(&self) -> Option<(Option<String>, Option<String>)> {
let contacts = self
.records
.iter()
.flat_map(|record| record.contributor.iter().flatten())
.filter(|contributor| contributor.contact)
.map(|contributor| {
(
contributor.id.clone().filter(|value| !value.trim().is_empty()),
contributor.email.clone().filter(|value| !value.trim().is_empty()),
)
})
.filter(|(identifier, email)| identifier.is_some() || email.is_some())
.collect::<BTreeSet<_>>();
(contacts.len() == 1).then(|| contacts.into_iter().next()).flatten()
}
pub fn organization_names(&self, sponsors: bool) -> Vec<String> {
self.records
.iter()
.flat_map(|record| {
let organizations = record.organization.as_deref().unwrap_or_default();
organizations.iter().filter(move |organization| {
let is_funder = organization.role.iter().any(|role| role.id.is_funder());
let is_partner =
(organizations.len() == 1 && organization.role.is_empty()) || organization.role.iter().any(|role| !role.id.is_funder());
match sponsors {
| true => is_funder,
| false => is_partner,
}
})
})
.filter_map(|organization| {
let vocabularies = match sponsors {
| true => ["sponsors", "partners"],
| false => ["partners", "sponsors"],
};
let ror = organization.id.trim_end_matches('/').rsplit('/').next().unwrap_or(&organization.id);
vocabularies.iter().flat_map(vocabulary::csv).find_map(|row| match row.as_slice() {
| [name, _, value, ..] if value.eq_ignore_ascii_case(ror) => Some(name.clone()),
| _ => None,
})
})
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
}
impl Metadata {
#[cfg(feature = "std")]
pub fn to_schema(format: &str) {
let schema = schema_for!(Metadata);
let output = match format.to_lowercase().as_str() {
| "yaml" | "yml" => serde_norway::to_string(&schema).unwrap_or_default(),
| _ => serde_json::to_string_pretty(&schema).unwrap_or_default(),
};
println!("{output}");
}
}
impl From<&str> for ObjectType {
fn from(value: &str) -> Self {
match value.trim().to_ascii_lowercase().replace([' ', '_'], "-").as_str() {
| "audiovisual" => Self::Audiovisual,
| "book" => Self::Book,
| "bookchapter" | "book-chapter" => Self::BookChapter,
| "computationalnotebook" | "computational-notebook" => Self::ComputationalNotebook,
| "conferencepaper" | "conference-paper" => Self::ConferencePaper,
| "conferenceproceeding" | "conference-proceeding" => Self::ConferenceProceeding,
| "datapaper" | "data-paper" => Self::DataPaper,
| "dataset" => Self::Dataset,
| "dissertation" => Self::Dissertation,
| "event" => Self::Event,
| "funding" => Self::Funding,
| "image" => Self::Image,
| "instrument" => Self::Instrument,
| "learningobject" | "learning-object" => Self::LearningObject,
| "model" => Self::Model,
| "outputmanagementplan" | "output-management-plan" => Self::OutputManagementPlan,
| "physicalobject" | "physical-object" => Self::PhysicalObject,
| "poster" | "conference-poster" => Self::ConferencePoster,
| "preprint" => Self::Preprint,
| "report" => Self::Report,
| "service" => Self::Service,
| "software" => Self::Software,
| "sound" => Self::Sound,
| "standard" => Self::Standard,
| "workflow" => Self::Workflow,
| _ => Self::Text,
}
}
}
impl Organization {
fn is_lead_research_organization(&self) -> bool {
self.role.iter().any(|role| role.id.is_lead_research_organization())
}
}
impl From<&str> for OrganizationRoleType {
fn from(value: &str) -> Self {
match value.trim().to_ascii_lowercase().replace([' ', '_'], "-").as_str() {
| "contractor" => Self::Contractor,
| "facility" => Self::Facility,
| "funder" => Self::Funder,
| "lead-research-organization" | "lead-research-organisation" => Self::LeadResearchOrganization,
| "other-research-organization" | "other-research-organisation" => Self::OtherResearchOrganization,
| "partner-organization" | "partner-organisation" => Self::PartnerOrganization,
| _ => Self::OtherOrganization,
}
}
}
impl TryFrom<&str> for PositionType {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.trim().to_ascii_lowercase().replace([' ', '_'], "-").as_str() {
| "co-investigator" => Ok(Self::CoInvestigator),
| "consultant" => Ok(Self::Consultant),
| "other" => Ok(Self::Other),
| "partner-investigator" => Ok(Self::PartnerInvestigator),
| "principal-investigator" => Ok(Self::PrincipalInvestigator),
| _ => Err("unknown contributor position"),
}
}
}
impl Default for RAID {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for RAID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let result = self.identifier();
write!(f, "{result}")
}
}
impl PersistentIdentifier for RAID {
fn new() -> Self {
RAID::init().build()
}
fn schema_uri(&self) -> String {
self.schema_uri.as_ref().cloned().unwrap_or_default().trim_end_matches("/").to_string()
}
fn prefix(&self) -> Option<String> {
self.prefix.clone()
}
fn suffix(&self) -> Option<String> {
self.suffix.clone()
}
fn identifier(&self) -> String {
let values = [self.prefix(), self.suffix()];
values
.iter()
.flatten()
.filter(|x| !x.is_empty())
.map(String::from)
.collect::<Vec<String>>()
.join("/")
}
}
impl PersistentIdentifierParse for RAID {
fn find_all(value: impl ToString) -> Vec<Self> {
let re = &RE_DOI;
re.find_iter(&value.to_string())
.filter_map(Result::ok)
.map(|m| RAID::from_string(trim_unmatched_trailing_parentheses(m.as_str())))
.collect()
}
fn format(value: impl ToString) -> String {
RAID::from_string(value).to_string()
}
fn from_string(value: impl ToString) -> Self {
let groups = ["schema_uri", "directory_indicator", "registrant_code", "suffix"];
let pattern = format!("^{RE_RAID_TEXT}$");
let text = value.to_string().trim().to_string();
let lookup = regex_capture_lookup(pattern.as_ref(), text.as_ref(), groups.to_vec());
let directory_indicator = lookup.get("directory_indicator").cloned();
let registrant_code = lookup.get("registrant_code").cloned();
let prefix = [directory_indicator, registrant_code]
.into_iter()
.flatten()
.collect::<Vec<String>>()
.join(".");
RAID::init()
.prefix(prefix)
.maybe_schema_uri(lookup.get("schema_uri").cloned())
.maybe_suffix(lookup.get("suffix").cloned())
.build()
}
fn is_valid(value: impl ToString) -> bool {
let pid = RAID::from_string(value.to_string());
let prefix_is_valid = match pid.prefix() {
| Some(x) => x.replace(".", "").is_numeric() && !x.eq("10.5555"),
| _ => false,
};
let suffix_is_valid = pid.suffix().is_some();
prefix_is_valid && suffix_is_valid
}
}
impl TryFrom<&str> for RelatedRaidType {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.trim().to_ascii_lowercase().replace([' ', '_'], "-").as_str() {
| "continues" => Ok(Self::Continues),
| "has-part" => Ok(Self::HasPart),
| "is-continued-by" => Ok(Self::IsContinuedBy),
| "is-derived-from" => Ok(Self::IsDerivedFrom),
| "is-obsoleted-by" => Ok(Self::IsObsoletedBy),
| "is-part-of" => Ok(Self::IsPartOf),
| "is-source-of" => Ok(Self::IsSourceOf),
| "obsoletes" => Ok(Self::Obsoletes),
| _ => Err("unknown related RAiD type"),
}
}
}
fn has_start_date(value: &Date) -> Result<(), ValidationError> {
match value.start_date.as_ref() {
| Some(start_date) if !start_date.trim().is_empty() => Ok(()),
| _ => Err(ValidationError::new("date").with_message("Provide valid start date")),
}
}
fn validate_contributor_identifier(value: &Contributor, _context: &()) -> Result<(), ValidationReport> {
match (value.id.as_deref(), value.schema_uri.as_deref().map(|uri| uri.trim_end_matches('/'))) {
| (Some(identifier), Some("https://orcid.org")) => identifier
.is_orcid()
.then_some(())
.ok_or_else(|| ValidationError::new("ORCiD").with_message("Provide valid ORCiD value"))
.map_err(|error| error.into_report("")),
| (Some(identifier), Some("https://isni.org")) => identifier
.is_isni()
.then_some(())
.ok_or_else(|| ValidationError::new("ISNI").with_message("Provide valid ISNI value"))
.map_err(|error| error.into_report("")),
| (Some(_), Some(_)) => Err(ValidationError::new("contributor.identifier")
.with_message("Contributor schemaUri must be https://orcid.org/ or https://isni.org/")
.into_report("")),
| _ => Ok(()),
}
}
fn validate_contributors(value: &Metadata, _context: &()) -> Result<(), ValidationReport> {
value.contributor.as_ref().map_or(Ok(()), |contributors| {
let contact = match contributors.iter().any(Contributor::is_contact) {
| true => ValidationReport::default(),
| false => ValidationError::new("contact")
.with_message("Mark at least one contributor as contact")
.into_report("contributor.contact"),
};
let leader = match contributors.iter().any(Contributor::is_leader) {
| true => ValidationReport::default(),
| false => ValidationError::new("leader")
.with_message("Mark at least one contributor as leader")
.into_report("contributor.leader"),
};
contact.merge("", leader).finish()
})
}
fn validate_metadata(value: &Metadata, context: &()) -> Result<(), ValidationReport> {
ValidationReport::default()
.merge("", report_of(validate_contributors(value, context)))
.merge("", report_of(validate_organization(value, context)))
.finish()
}
fn validate_organization(value: &Metadata, _context: &()) -> Result<(), ValidationReport> {
value.organization.as_ref().map_or(Ok(()), |organizations| {
let lead_count = organizations
.iter()
.filter(|organization| organization.is_lead_research_organization())
.count();
match (organizations.is_empty(), lead_count) {
| (true, _) | (_, 1) => Ok(()),
| (_, 0) => Err(ValidationError::new("organization")
.with_message("Mark one organization as lead research organization")
.into_report("organization")),
| _ => Err(ValidationError::new("organization")
.with_message("Only one organization can be lead research organization")
.into_report("organization")),
}
})
}
#[cfg(all(test, feature = "std"))]
mod tests;