use crate::pid::raid::CreditRole;
use crate::research_activity::common::{ControlledSubject, StructuredRights, TypedActivityDate, TypedIdentifier, TypedRelatedResource};
use crate::standard::datacite::{self, Description, GeoLocation, Publisher, ResourceTypeGeneral};
use crate::validation::{is_iso_639_1_language_code, rules::nonempty, Property, PropertySet, Validate};
use crate::{Keyword, Website};
use acorn_core::prelude::alloc::{String, Vec};
use bon::Builder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_trim::{option_string_trim, string_trim};
use serde_with::skip_serializing_none;
#[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 Access {
pub open: bool,
#[serde(default, deserialize_with = "option_string_trim")]
pub status: Option<String>,
#[validate(url)]
pub url: Option<String>,
#[serde(default)]
pub license: Option<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 Affiliation {
#[validate(length(min = 1))]
#[serde(deserialize_with = "string_trim")]
pub name: String,
#[validate(ror)]
#[serde(default, deserialize_with = "option_string_trim")]
pub ror: Option<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 Award {
#[serde(deserialize_with = "string_trim")]
pub identifier: String,
#[serde(default, deserialize_with = "option_string_trim")]
pub title: Option<String>,
#[validate(doi)]
#[serde(default, deserialize_with = "option_string_trim")]
pub doi: Option<String>,
#[validate(url)]
pub url: Option<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(length(min = 1))]
#[serde(deserialize_with = "string_trim")]
pub name: String,
pub name_type: Option<datacite::NameType>,
#[validate(orcid)]
#[serde(default, deserialize_with = "option_string_trim")]
pub orcid: Option<String>,
#[validate(ror)]
#[serde(default, deserialize_with = "option_string_trim")]
pub ror: Option<String>,
pub contributor_type: Option<datacite::ContributorType>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub roles: Vec<CreditRole>,
pub corresponding: Option<bool>,
#[serde(default, deserialize_with = "option_string_trim")]
pub position: Option<String>,
#[validate(nested)]
#[builder(default)]
pub affiliations: Vec<Affiliation>,
}
#[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 Funding {
#[validate(length(min = 1))]
#[serde(deserialize_with = "string_trim")]
pub name: String,
#[serde(default, deserialize_with = "option_string_trim")]
pub identifier: Option<String>,
pub identifier_type: Option<datacite::FunderIdentifierType>,
#[validate(url)]
#[serde(default, deserialize_with = "option_string_trim")]
pub scheme_uri: Option<String>,
#[validate(ror)]
#[serde(default, deserialize_with = "option_string_trim")]
pub ror: Option<String>,
#[validate(nested)]
#[builder(default)]
pub awards: Vec<Award>,
}
#[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 ResearchOutput {
#[serde(default, deserialize_with = "option_string_trim")]
pub identifier: Option<String>,
#[validate(doi)]
#[serde(default, deserialize_with = "option_string_trim")]
pub doi: Option<String>,
#[validate(length(min = 1))]
#[serde(deserialize_with = "string_trim")]
pub title: String,
#[serde(rename = "type", default, deserialize_with = "option_string_trim")]
pub kind: Option<String>,
#[validate(year)]
#[serde(rename = "publicationYear")]
pub publication_year: Option<i32>,
#[validate(nested)]
pub dates: Option<Vec<TypedActivityDate>>,
#[validate(nested)]
pub publisher: Option<Publisher>,
pub resource_type_general: Option<ResourceTypeGeneral>,
#[serde(default, deserialize_with = "option_string_trim")]
pub resource_type: Option<String>,
#[validate(custom(function = "is_iso_639_1_language_code"))]
#[serde(default, deserialize_with = "option_string_trim")]
pub language: Option<String>,
#[validate(nested)]
pub alternate_identifiers: Option<Vec<TypedIdentifier>>,
#[validate(nested)]
pub related_identifiers: Option<Vec<TypedRelatedResource>>,
#[validate(nested)]
pub related_items: Option<Vec<datacite::RelatedItem>>,
#[validate(version)]
#[serde(default, deserialize_with = "option_string_trim")]
pub version: Option<String>,
#[validate(nested)]
pub rights_list: Option<Vec<StructuredRights>>,
#[validate(nested)]
pub descriptions: Option<Vec<Description>>,
#[validate(nested)]
pub subjects: Option<Vec<ControlledSubject>>,
pub formats: Option<Vec<String>>,
pub sizes: Option<Vec<String>>,
#[validate(nested)]
pub geo_locations: Option<Vec<GeoLocation>>,
#[validate(nested)]
pub creators: Option<Vec<Contributor>>,
#[validate(nested)]
pub contributors: Option<Vec<Contributor>>,
#[validate(nested)]
pub funding: Option<Vec<Funding>>,
#[validate(nested)]
pub websites: Option<Vec<Website>>,
#[builder(default)]
pub keywords: Vec<Keyword>,
#[validate(nested)]
pub access: Option<Access>,
}
impl ResearchOutput {
pub fn validate_deposit_ready(&self) -> Result<(), Vec<String>> {
let ResearchOutput {
doi,
title,
creators,
publisher,
publication_year,
resource_type_general,
..
} = self;
let properties = [
Property::new("doi", doi.as_deref().is_some_and(|value| nonempty(value).is_ok())),
Property::new("titles", nonempty(title).is_ok()),
Property::new(
"creators",
creators
.as_ref()
.is_some_and(|creators| creators.iter().map(|creator| creator.name.as_str()).any(|name| nonempty(name).is_ok())),
),
Property::new(
"publisher",
publisher
.as_ref()
.map(|publisher| publisher.name.as_str())
.is_some_and(|name| nonempty(name).is_ok()),
),
Property::new("publicationYear", publication_year.is_some()),
Property::new("resourceType", resource_type_general.is_some()),
];
PropertySet::new(properties).validate()
}
pub fn merge(self, candidate: Self) -> Self {
Self {
identifier: self.identifier.filter(|value| !value.trim().is_empty()).or(candidate.identifier),
doi: self.doi.filter(|value| !value.trim().is_empty()).or(candidate.doi),
title: match self.title.trim().is_empty() {
| true => candidate.title,
| false => self.title,
},
kind: self.kind.filter(|value| !value.trim().is_empty()).or(candidate.kind),
publication_year: self.publication_year.or(candidate.publication_year),
dates: self.dates.filter(|values| !values.is_empty()).or(candidate.dates),
publisher: self.publisher.or(candidate.publisher),
resource_type_general: self.resource_type_general.or(candidate.resource_type_general),
resource_type: self.resource_type.filter(|value| !value.trim().is_empty()).or(candidate.resource_type),
language: self.language.filter(|value| !value.trim().is_empty()).or(candidate.language),
alternate_identifiers: self
.alternate_identifiers
.filter(|values| !values.is_empty())
.or(candidate.alternate_identifiers),
related_identifiers: self
.related_identifiers
.filter(|values| !values.is_empty())
.or(candidate.related_identifiers),
related_items: self.related_items.filter(|values| !values.is_empty()).or(candidate.related_items),
version: self.version.filter(|value| !value.trim().is_empty()).or(candidate.version),
rights_list: self.rights_list.filter(|values| !values.is_empty()).or(candidate.rights_list),
descriptions: self.descriptions.filter(|values| !values.is_empty()).or(candidate.descriptions),
subjects: self.subjects.filter(|values| !values.is_empty()).or(candidate.subjects),
formats: self.formats.filter(|values| !values.is_empty()).or(candidate.formats),
sizes: self.sizes.filter(|values| !values.is_empty()).or(candidate.sizes),
geo_locations: self.geo_locations.filter(|values| !values.is_empty()).or(candidate.geo_locations),
creators: self.creators.filter(|values| !values.is_empty()).or(candidate.creators),
contributors: self.contributors.filter(|values| !values.is_empty()).or(candidate.contributors),
funding: self.funding.filter(|values| !values.is_empty()).or(candidate.funding),
websites: self.websites.filter(|values| !values.is_empty()).or(candidate.websites),
keywords: match self.keywords.is_empty() {
| true => candidate.keywords,
| false => self.keywords,
},
access: self.access.or(candidate.access),
}
}
}