use std::fmt;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::domain::sector::{Sector, SectorData};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ComplianceFinding {
pub code: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub field: String,
pub message: String,
}
impl ComplianceFinding {
pub fn new(
code: impl Into<String>,
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
code: code.into(),
field: field.into(),
message: message.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ComplianceResult {
pub co2e_score: Option<f64>,
pub repairability_index: Option<f64>,
pub recycled_content_pct: Option<f64>,
pub compliance_status: ComplianceStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub violations: Vec<ComplianceFinding>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<ComplianceFinding>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ruleset_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assessed_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub receipt: Option<serde_json::Value>,
}
impl Default for ComplianceResult {
fn default() -> Self {
Self {
co2e_score: None,
repairability_index: None,
recycled_content_pct: None,
compliance_status: ComplianceStatus::PassthroughNoValidation,
violations: Vec::new(),
warnings: Vec::new(),
ruleset_version: None,
assessed_at: None,
receipt: None,
}
}
}
impl ComplianceResult {
#[must_use]
pub fn passthrough() -> Self {
Self::default()
}
#[must_use]
pub fn with_status(status: ComplianceStatus) -> Self {
Self {
compliance_status: status,
..Self::default()
}
}
#[must_use]
pub fn has_violations(&self) -> bool {
!self.violations.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum ComplianceStatus {
PassthroughNoValidation,
Compliant,
NonCompliant,
NotAssessed,
NotImplemented,
}
#[must_use]
pub fn gate_determination(in_force: bool, raw: ComplianceStatus) -> ComplianceStatus {
if in_force {
return raw;
}
match raw {
ComplianceStatus::Compliant | ComplianceStatus::NonCompliant => {
ComplianceStatus::NotAssessed
}
other => other,
}
}
#[derive(Debug, Clone)]
pub struct ComplianceError {
pub kind: ComplianceErrorKind,
pub message: String,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ComplianceErrorKind {
UnknownSector,
InvalidInput,
Internal,
}
impl fmt::Display for ComplianceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}: {}", self.kind, self.message)
}
}
impl std::error::Error for ComplianceError {}
pub trait ComplianceStrategy: Send + Sync {
fn sector(&self) -> Sector;
fn compute(&self, data: &SectorData) -> Result<ComplianceResult, ComplianceError>;
}
pub trait ComplianceRegistry: Send + Sync {
fn compute(
&self,
sector: Sector,
data: &SectorData,
) -> Result<ComplianceResult, ComplianceError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provisional_downgrades_binding_determinations() {
assert_eq!(
gate_determination(false, ComplianceStatus::Compliant),
ComplianceStatus::NotAssessed
);
assert_eq!(
gate_determination(false, ComplianceStatus::NonCompliant),
ComplianceStatus::NotAssessed
);
}
#[test]
fn in_force_preserves_determinations() {
assert_eq!(
gate_determination(true, ComplianceStatus::Compliant),
ComplianceStatus::Compliant
);
assert_eq!(
gate_determination(true, ComplianceStatus::NonCompliant),
ComplianceStatus::NonCompliant
);
}
#[test]
fn non_binding_statuses_pass_through_regardless() {
for in_force in [true, false] {
assert_eq!(
gate_determination(in_force, ComplianceStatus::PassthroughNoValidation),
ComplianceStatus::PassthroughNoValidation
);
assert_eq!(
gate_determination(in_force, ComplianceStatus::NotAssessed),
ComplianceStatus::NotAssessed
);
}
}
}