use super::{ConformanceResult, ContributionKind, StandardReference};
use crate::validation::{rules, IntoValidationReport, Validate, ValidationReport};
use acorn_core::prelude::alloc::{String, ToString};
use acorn_core::validation::ValidationError;
use bon::Builder;
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_trim::{option_string_trim, string_trim};
use serde_with::skip_serializing_none;
use strum::EnumIs;
#[derive(Clone, Debug, EnumIs, Eq, PartialEq)]
pub enum RecognitionProgram {
Accepted,
Inspired,
IntegratedSolution,
Other(String),
Ready,
Safe,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Contribution {
#[validate(nested)]
pub identifier: ContributionId,
#[validate(nonempty)]
#[serde(deserialize_with = "string_trim")]
pub title: String,
#[validate(nested)]
pub project: Project,
#[validate(nested)]
pub kind: ContributionKind,
#[validate(nonempty, version)]
#[serde(default, deserialize_with = "option_string_trim")]
pub version: Option<String>,
#[validate(nonempty)]
#[serde(default, deserialize_with = "option_string_trim")]
pub status: Option<String>,
#[validate(uri)]
#[serde(deserialize_with = "string_trim")]
pub canonical_uri: String,
#[validate(nonempty)]
#[serde(default, deserialize_with = "option_string_trim")]
pub license: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct ContributionId(#[serde(deserialize_with = "string_trim")] pub String);
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[validate(schema(function = "ManagementProfileClaim::is_valid", skip_on_field_errors = false))]
pub struct ManagementProfileClaim {
#[validate(nested)]
pub profile: StandardReference,
#[validate(nested)]
pub underlying_standard: StandardReference,
#[validate(nonempty)]
#[serde(default, deserialize_with = "option_string_trim")]
pub validator_identity: Option<String>,
#[validate(nonempty)]
#[serde(default, deserialize_with = "option_string_trim")]
pub validator_version: Option<String>,
#[validate(timestamp)]
#[serde(default, deserialize_with = "option_string_trim")]
pub tested_at: Option<String>,
pub result: ConformanceResult,
#[validate(uri)]
#[serde(default, deserialize_with = "option_string_trim")]
pub evidence_uri: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct Project(#[serde(deserialize_with = "string_trim")] pub String);
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[validate(schema(function = "Recognition::is_valid", skip_on_field_errors = false))]
pub struct Recognition {
#[validate(nested)]
pub program: RecognitionProgram,
#[validate(nonempty)]
#[serde(deserialize_with = "string_trim")]
pub subject_identifier: String,
#[validate(uri)]
#[serde(deserialize_with = "string_trim")]
pub source_uri: String,
#[validate(timestamp)]
#[serde(default, deserialize_with = "option_string_trim")]
pub awarded_at: Option<String>,
#[validate(timestamp)]
#[serde(default, deserialize_with = "option_string_trim")]
pub expires_at: Option<String>,
}
impl From<&str> for ContributionId {
fn from(value: &str) -> Self {
Self::from(value.to_string())
}
}
impl From<String> for ContributionId {
fn from(value: String) -> Self {
Self(value.trim().to_string())
}
}
impl Validate for ContributionId {
fn validate(&self) -> Result<(), ValidationReport> {
self.0
.strip_prefix('S')
.filter(|digits| !digits.is_empty() && digits.chars().all(|character| character.is_ascii_digit()))
.map(|_| ())
.ok_or_else(|| ValidationError::new("ocp_contribution_id").with_message("Provide an OCP identifier such as S0806"))
.map_err(|error| error.into_report(""))
}
}
impl ManagementProfileClaim {
fn is_valid(&self, _context: &()) -> Result<(), ValidationReport> {
match self.result {
| ConformanceResult::NotTested => Ok(()),
| ConformanceResult::Fail | ConformanceResult::Partial | ConformanceResult::Pass => {
let report = [
self.tested_at.is_none().then(|| {
ValidationReport::from_error(
"tested_at",
ValidationError::new("tested_at_required").with_message("Tested results require testedAt"),
)
}),
self.evidence_uri.is_none().then(|| {
ValidationReport::from_error(
"evidence_uri",
ValidationError::new("evidence_required").with_message("Tested results require evidenceUri"),
)
}),
]
.into_iter()
.flatten()
.fold(ValidationReport::new(), |report, issue| report.merge("", issue));
report.finish()
}
}
}
}
impl From<&str> for Project {
fn from(value: &str) -> Self {
Self::from(value.to_string())
}
}
impl From<String> for Project {
fn from(value: String) -> Self {
Self(value.trim().to_string())
}
}
impl Validate for Project {
fn validate(&self) -> Result<(), ValidationReport> {
rules::nonempty(&self.0).map_err(|error| error.into_report(""))
}
}
impl Recognition {
fn is_valid(&self, _context: &()) -> Result<(), ValidationReport> {
match (&self.awarded_at, &self.expires_at) {
| (Some(awarded_at), Some(expires_at)) => match (rules::parse_timestamp(awarded_at), rules::parse_timestamp(expires_at)) {
| (Ok(awarded_at), Ok(expires_at)) if expires_at <= awarded_at => ValidationError::new("timestamp_order")
.with_message("expiresAt must follow awardedAt")
.into_report("expires_at")
.finish(),
| _ => Ok(()),
},
| _ => Ok(()),
}
}
}
impl RecognitionProgram {
pub(crate) fn is_environment_recognition(&self) -> bool {
self.is_integrated_solution() || self.is_other()
}
pub(crate) fn is_facility_recognition(&self) -> bool {
self.is_other() || self.is_ready()
}
pub(crate) fn is_platform_recognition(&self) -> bool {
self.is_accepted() || self.is_inspired() || self.is_other() || self.is_safe()
}
}
impl<'de> Deserialize<'de> for RecognitionProgram {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
String::deserialize(deserializer).map(Self::from)
}
}
impl From<String> for RecognitionProgram {
fn from(value: String) -> Self {
let value = value.trim().to_string();
match value.as_str() {
| "accepted" | "Accepted" => Self::Accepted,
| "inspired" | "Inspired" => Self::Inspired,
| "integratedSolution" | "Integrated Solution" => Self::IntegratedSolution,
| "ready" | "Ready" => Self::Ready,
| "safe" | "SAFE" | "S.A.F.E." => Self::Safe,
| _ => Self::Other(value),
}
}
}
impl JsonSchema for RecognitionProgram {
fn schema_name() -> alloc::borrow::Cow<'static, str> {
"RecognitionProgram".into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
String::json_schema(generator)
}
}
impl Serialize for RecognitionProgram {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(match self {
| Self::Accepted => "accepted",
| Self::Inspired => "inspired",
| Self::IntegratedSolution => "integratedSolution",
| Self::Other(value) => value,
| Self::Ready => "ready",
| Self::Safe => "safe",
})
}
}
impl Validate for RecognitionProgram {
fn validate(&self) -> Result<(), ValidationReport> {
match self {
| Self::Other(value) => rules::nonempty(value).map_err(|error| error.into_report("")),
| _ => Ok(()),
}
}
}
impl From<&Contribution> for StandardReference {
fn from(contribution: &Contribution) -> Self {
Self {
authority: "Open Compute Project".to_string(),
identifier: contribution.identifier.0.clone(),
title: Some(contribution.title.clone()),
version: contribution.version.clone(),
canonical_uri: contribution.canonical_uri.clone(),
}
}
}