#[cfg(feature = "std")]
use crate::io::current_date;
#[cfg(feature = "std")]
use crate::io::{read_file, License};
use crate::prelude::*;
#[cfg(feature = "std")]
use crate::prelude::{Error, PathBuf};
#[cfg(feature = "std")]
use crate::schema::namespaces::{DEFAULT_ORCID_SCHEMA_URI, DEFAULT_ROR_SCHEMA_URI};
#[cfg(feature = "std")]
use crate::schema::research_activity::ResearchActivity;
use crate::schema::validate::{has_at_least_one_truthy, is_date, is_orcid, is_raid, is_ror, is_unix_epoch, is_year};
use crate::schema::Date;
#[cfg(feature = "std")]
use crate::util::Label;
#[cfg(not(feature = "std"))]
use crate::util::License;
use bon::Builder;
use derive_more::Display;
#[cfg(feature = "std")]
use schemars::schema_for;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "std")]
use tracing::error;
use validator::{Validate, ValidationError};
#[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, 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, 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(custom(function = "is_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))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Contributor {
#[validate(required, custom(function = "is_orcid"))]
pub id: Option<String>,
#[validate(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,
}
#[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, custom(function = "is_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, nested)]
pub license: Option<License>,
#[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(custom(function = "is_unix_epoch"))]
pub created: usize,
#[validate(custom(function = "is_unix_epoch"))]
pub updated: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Organization {
#[validate(custom(function = "is_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(custom(function = "is_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, 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>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RegistrationAgency {
#[validate(custom(function = "is_ror"))]
pub id: String,
#[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, custom(function = "is_date"))]
pub start_date: Option<String>,
#[validate(custom(function = "is_year"))]
pub end_date: Option<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>,
}
#[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,
}
impl Metadata {
#[cfg(feature = "std")]
pub fn to_schema() {
let schema = schema_for!(Metadata);
match serde_json::to_string_pretty(&schema) {
| Ok(json) => println!("{}", json),
| Err(why) => eprintln!("Failed to serialize schema: {}", why),
}
}
#[cfg(feature = "std")]
pub fn read(path: PathBuf) -> Result<Metadata, Error> {
match read_file(path) {
| Ok(data) => match eserde::json::from_str::<Metadata>(&data) {
| Ok(value) => Ok(value),
| Err(errors) => {
let details: Vec<String> = errors
.iter()
.map(|e| format!("{}: {}", e.path().map_or("root".into(), |p| p.to_string()), e.message()))
.collect();
Err(Error::other(details.join("\n")))
}
},
| Err(why) => {
let msg = format!("Read RAiD metadata file - {why}");
error!("=> {} {}", Label::fail(), msg);
Err(Error::other(msg))
}
}
}
}
impl Contributor {
fn is_contact(&self) -> bool {
self.contact
}
fn is_leader(&self) -> bool {
self.leader
}
}
impl Organization {
fn is_lead_research_organization(&self) -> bool {
self.role
.iter()
.any(|role| matches!(role.id, OrganizationRoleType::LeadResearchOrganization))
}
}
#[cfg(feature = "std")]
impl From<ResearchActivity> for Metadata {
fn from(activity: ResearchActivity) -> Self {
let default_start_date = current_date();
let default_end_year = default_start_date.chars().take(4).collect::<String>();
let raid_id = activity.meta.raid.as_ref().and_then(|raid| raid.first()).cloned().unwrap_or_default();
let ror_id = activity.meta.ror.as_ref().and_then(|rors| rors.first()).cloned().unwrap_or_default();
let contributor_id = activity.contact.identifier.clone().filter(|id| is_orcid(id).is_ok()).unwrap_or_default();
let date = Date {
start_date: Some(default_start_date.clone()),
end_date: None,
};
let contributor_position = ContributorPosition::init()
.id(PositionType::PrincipalInvestigator)
.date(date.clone())
.build();
let contributor = Contributor {
id: Some(contributor_id),
schema_uri: Some(DEFAULT_ORCID_SCHEMA_URI.to_string()),
status: Some("active".to_string()),
status_message: None,
position: vec![contributor_position],
leader: true,
contact: true,
email: Some(activity.contact.email.clone()),
role: None,
uuid: None,
};
let org_role = OrganizationRole {
id: OrganizationRoleType::LeadResearchOrganization,
schema_uri: None,
date: date.clone(),
};
let organization = Organization {
id: ror_id.clone(),
schema_uri: Some(DEFAULT_ROR_SCHEMA_URI.to_string()),
role: vec![org_role],
};
let title = Title {
text: Some(activity.title.clone()),
title_type: Some(TitleIdentifier::init().id(TitleType::Primary).build()),
language: None,
start_date: Some(default_start_date.clone()),
end_date: Some(default_end_year.clone()),
};
let description = Description {
text: Some(activity.sections.mission.clone()),
description_type: Some(DescriptionIdentifier {
id: DescriptionType::Primary,
schema_uri: "https://vocabulary.raid.org/description.type.schema/318".to_string(),
}),
language: None,
};
let identifier = MetadataIdentifier {
id: Some(raid_id),
schema_uri: Some("https://raid.org/".to_string()),
owner: Some(Owner {
id: ror_id.clone(),
schema_uri: Some(DEFAULT_ROR_SCHEMA_URI.to_string()),
service_point: 0,
}),
raid_agency_url: Some("https://raid.org/".to_string()),
registration_agency: Some(RegistrationAgency {
id: ror_id,
schema_uri: Some(DEFAULT_ROR_SCHEMA_URI.to_string()),
}),
license: Some(License::Single("CC0-1.0".to_string())),
version: Some(1),
};
let access = Access {
access_type: Some(AccessIdentifier {
id: AccessType::OpenAccess,
schema_uri: "https://vocabularies.coar-repositories.org/access_rights/".to_string(),
}),
embargo_expiry: None,
statement: None,
};
Metadata {
access: Some(access),
contributor: Some(vec![contributor]),
date: None,
description: Some(vec![description]),
metadata: None,
title: Some(vec![title]),
identifier: Some(identifier),
alternate_identifier: None,
alternate_url: None,
organization: Some(vec![organization]),
related_object: None,
related_raid: None,
spatial_coverage: None,
subject: None,
traditional_knowledge_label: None,
}
}
}
fn has_contact(value: &Vec<Contributor>) -> Result<(), ValidationError> {
has_at_least_one_truthy(
value.as_slice(),
Contributor::is_contact,
"contributors",
"Mark at least one contributor as contact",
)
}
fn has_leader(value: &Vec<Contributor>) -> Result<(), ValidationError> {
has_at_least_one_truthy(
value.as_slice(),
Contributor::is_leader,
"contributors",
"Mark at least one contributor as leader",
)
}
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".into())),
}
}
fn validate_contributors(value: &Metadata) -> Result<(), ValidationError> {
match &value.contributor {
| Some(contributors) => {
let message = [
has_contact(contributors)
.err()
.map(|_| "Mark at least one contributor as contact".to_string()),
has_leader(contributors)
.err()
.map(|_| "Mark at least one contributor as leader".to_string()),
]
.into_iter()
.flatten()
.collect::<Vec<String>>()
.join("; ");
match message.is_empty() {
| true => Ok(()),
| false => Err(ValidationError::new("contributors").with_message(message.into())),
}
}
| None => Ok(()),
}
}
fn validate_organization(value: &Metadata) -> Result<(), ValidationError> {
match &value.organization {
| Some(organizations) => {
let lead_count = organizations
.iter()
.filter(|organization| organization.is_lead_research_organization())
.count();
let message = if organizations.is_empty() {
None
} else if lead_count == 0 {
Some("Mark one organization as lead research organization".to_string())
} else if lead_count > 1 {
Some("Only one organization can be lead research organization".to_string())
} else {
None
};
match message {
| Some(value) => Err(ValidationError::new("organization").with_message(value.into())),
| None => Ok(()),
}
}
| None => Ok(()),
}
}
fn validate_metadata(value: &Metadata) -> Result<(), ValidationError> {
let message = [
validate_contributors(value)
.err()
.map(|why| why.message.unwrap_or_else(|| "Invalid contributors".into()).to_string()),
validate_organization(value)
.err()
.map(|why| why.message.unwrap_or_else(|| "Invalid organization".into()).to_string()),
]
.into_iter()
.flatten()
.collect::<Vec<String>>()
.join("; ");
match message.is_empty() {
| true => Ok(()),
| false => Err(ValidationError::new("metadata").with_message(message.into())),
}
}