use std::collections::HashMap;
use chrono::NaiveDate;
use super::passthrough_strategies::{PassthroughBatteryStrategy, PassthroughTextileStrategy};
use crate::{
domain::sector::SectorData,
ports::compliance::{
ComplianceError, ComplianceRegistry, ComplianceResult, ComplianceStrategy,
},
};
pub struct PassthroughRegistry {
strategies: HashMap<String, Box<dyn ComplianceStrategy>>,
}
impl PassthroughRegistry {
#[must_use]
pub fn new() -> Self {
let mut registry = Self {
strategies: HashMap::new(),
};
registry.register(Box::new(PassthroughBatteryStrategy));
registry.register(Box::new(PassthroughTextileStrategy));
registry
}
#[must_use]
pub fn empty() -> Self {
Self {
strategies: HashMap::new(),
}
}
pub fn register(&mut self, strategy: Box<dyn ComplianceStrategy>) {
self.strategies
.insert(strategy.sector_key().to_owned(), strategy);
}
#[must_use]
pub fn registered_sectors(&self) -> Vec<&str> {
let mut keys: Vec<&str> = self.strategies.keys().map(String::as_str).collect();
keys.sort_unstable();
keys
}
}
impl Default for PassthroughRegistry {
fn default() -> Self {
Self::new()
}
}
impl ComplianceRegistry for PassthroughRegistry {
fn compute(
&self,
sector_key: &str,
data: &SectorData,
law_in_force_on: Option<NaiveDate>,
) -> Result<ComplianceResult, ComplianceError> {
match self.strategies.get(sector_key) {
Some(strategy) => strategy.compute(data, law_in_force_on),
None => Ok(ComplianceResult::passthrough()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::sector::{BatteryData, FibreEntry, Sector, SectorData, TextileData};
use crate::ports::compliance::ComplianceStatus;
fn battery_data() -> SectorData {
SectorData::Battery(Box::new(BatteryData {
recycled_content_lithium_pct: Some(12.5),
rated_capacity_kwh: Some(32.0),
..crate::test_support::sample_battery_data()
}))
}
fn textile_data() -> SectorData {
SectorData::Textile(Box::new(TextileData {
fibre_composition: vec![FibreEntry {
fibre: "cotton".into(),
pct: 100.0,
country_of_origin: None,
}],
country_of_origin: "BD".into(),
care_instructions: "40°C wash".into(),
chemical_compliance_standard: "OEKO-TEX 100".into(),
recycled_content_pct: Some(30.0),
carbon_footprint_kg_co2e: Some(8.5),
repair_score: Some(7.5),
..crate::test_support::sample_textile_data()
}))
}
#[test]
fn passthrough_makes_no_determination_for_any_sector() {
let registry = PassthroughRegistry::new();
for (sector, data) in [
(Sector::Battery, battery_data()),
(Sector::Textile, textile_data()),
(Sector::Electronics, battery_data()),
] {
let result = registry.compute(sector.catalog_key(), &data, None).unwrap();
assert_eq!(
result.compliance_status,
ComplianceStatus::PassthroughNoValidation,
"{sector:?} must not receive a determination"
);
assert!(
result.violations.is_empty() && result.warnings.is_empty(),
"{sector:?} passthrough must produce no findings"
);
assert!(
result.receipt.is_none() && result.ruleset_version.is_none(),
"{sector:?} ran no calculation, so it has no receipt to show for one"
);
}
}
#[test]
fn a_registered_sector_uses_its_strategy_and_the_rest_fall_back() {
let registry = PassthroughRegistry::new();
assert_eq!(registry.registered_sectors(), vec!["battery", "textile"]);
let textile = registry.compute("textile", &textile_data(), None).unwrap();
assert_eq!(textile.co2e_score, Some(8.5));
assert_eq!(textile.recycled_content_pct, Some(30.0));
assert_eq!(textile.repairability_index, Some(7.5));
let electronics = registry
.compute("electronics", &battery_data(), None)
.unwrap();
assert_eq!(electronics.co2e_score, None);
assert_eq!(
electronics.compliance_status,
ComplianceStatus::PassthroughNoValidation
);
let unknown = registry.compute("quantum-widget", &battery_data(), None);
assert!(
unknown.is_ok(),
"an unmodelled sector must not be an error here"
);
}
#[test]
fn registering_a_strategy_replaces_the_one_already_there() {
struct AlwaysFortyTwo;
impl ComplianceStrategy for AlwaysFortyTwo {
fn sector_key(&self) -> &str {
"textile"
}
fn compute(
&self,
_: &SectorData,
_: Option<NaiveDate>,
) -> Result<ComplianceResult, ComplianceError> {
Ok(ComplianceResult {
co2e_score: Some(42.0),
..ComplianceResult::passthrough()
})
}
}
let mut registry = PassthroughRegistry::new();
registry.register(Box::new(AlwaysFortyTwo));
assert_eq!(
registry
.compute("textile", &textile_data(), None)
.unwrap()
.co2e_score,
Some(42.0),
"the registered strategy must displace the built-in one"
);
assert_eq!(
registry.registered_sectors(),
vec!["battery", "textile"],
"replacing must not add a second entry for the same sector"
);
}
#[test]
fn an_empty_registry_falls_back_for_everything() {
let registry = PassthroughRegistry::empty();
assert!(registry.registered_sectors().is_empty());
let result = registry.compute("textile", &textile_data(), None).unwrap();
assert_eq!(result.co2e_score, None);
assert_eq!(
result.compliance_status,
ComplianceStatus::PassthroughNoValidation
);
}
}