use std::sync::OnceLock;
use semver::Version;
use super::rules::{
battery_recycled_chemistry_conflicts, validate_battery_operating_temp,
validate_fibre_composition, validate_surfactants, validate_svhc_substances,
};
use super::validator::ProductGroupValidatorRegistry;
use crate::catalog::ProductGroupCatalog;
use crate::error::DppError;
use crate::field_error::{FieldError, ValidationErrors};
use crate::passport::Passport;
use crate::product_group::{ProductGroupData, SvhcSubstance};
use crate::schemas::VersionedSchemaRegistry;
fn default_registry() -> &'static VersionedSchemaRegistry {
static REGISTRY: OnceLock<VersionedSchemaRegistry> = OnceLock::new();
REGISTRY.get_or_init(VersionedSchemaRegistry::new)
}
fn default_catalog() -> &'static ProductGroupCatalog {
static CATALOG: OnceLock<ProductGroupCatalog> = OnceLock::new();
CATALOG.get_or_init(ProductGroupCatalog::new)
}
pub fn validate_product_group_data(
product_group_data: &ProductGroupData,
) -> Result<(), ValidationErrors> {
validate_product_group_data_with_registry(
product_group_data,
&ProductGroupValidatorRegistry::default(),
)
}
pub fn validate_product_group_data_with_registry(
product_group_data: &ProductGroupData,
registry: &ProductGroupValidatorRegistry,
) -> Result<(), ValidationErrors> {
let mut errors: Vec<FieldError> = Vec::new();
if let ProductGroupData::Other { product_group, .. } = product_group_data {
if let Err(ve) = validate_raw_product_group_data(
product_group,
&product_group_data_instance(product_group_data),
registry,
) {
errors.extend(ve.errors);
}
} else {
schema_errors(product_group_data, &mut errors);
cross_field_errors(product_group_data, &mut errors);
}
if errors.is_empty() {
Ok(())
} else {
Err(ValidationErrors { errors })
}
}
pub fn validate_raw_product_group_data(
product_group_key: &str,
data: &serde_json::Value,
registry: &ProductGroupValidatorRegistry,
) -> Result<(), ValidationErrors> {
let mut errors: Vec<FieldError> = Vec::new();
let catalog = default_catalog();
let has_schema = catalog.current_schema_version(product_group_key).is_some();
if let Some(version_str) = catalog.current_schema_version(product_group_key) {
match version_str.parse::<semver::Version>() {
Ok(version) => {
if let Err(ve) = default_registry().validate(product_group_key, &version, data) {
errors.extend(ve.errors);
}
}
Err(_) => errors.push(FieldError {
field: "/schemaVersion".to_owned(),
message: format!(
"product_group '{product_group_key}' has an invalid current schema version '{version_str}'"
),
}),
}
}
match registry.get(product_group_key) {
Some(v) => {
if let Err(field_errors) = v.validate(data) {
errors.extend(field_errors);
}
}
None if !has_schema => {
errors.push(FieldError {
field: "/product_group".to_owned(),
message: format!(
"unknown product_group \"{product_group_key}\": no JSON schema or cross-field validator registered"
),
});
}
None => {}
}
if errors.is_empty() {
Ok(())
} else {
Err(ValidationErrors { errors })
}
}
fn schema_errors(product_group_data: &ProductGroupData, errors: &mut Vec<FieldError>) {
let product_group = product_group_data.product_group();
let key = product_group.catalog_key();
let Some(version_str) = default_catalog().current_schema_version(key) else {
return;
};
let version = match version_str.parse::<Version>() {
Ok(v) => v,
Err(_) => {
errors.push(FieldError {
field: "/schemaVersion".to_owned(),
message: format!(
"product_group '{key}' has an invalid current schema version '{version_str}'"
),
});
return;
}
};
let instance = product_group_data_instance(product_group_data);
if let Err(ve) = default_registry().validate(key, &version, &instance) {
errors.extend(ve.errors);
}
}
fn product_group_data_instance(product_group_data: &ProductGroupData) -> serde_json::Value {
let mut value =
serde_json::to_value(product_group_data).expect("ProductGroupData serializes to Value");
if let Some(obj) = value.as_object_mut() {
obj.remove("productGroup");
}
value
}
fn cross_field_errors(product_group_data: &ProductGroupData, errors: &mut Vec<FieldError>) {
match product_group_data {
ProductGroupData::Battery(d) => {
if let Err(msg) =
validate_battery_operating_temp(d.operating_temp_min_c, d.operating_temp_max_c)
{
errors.push(FieldError {
field: "/operatingTempMinC".to_owned(),
message: msg,
});
}
let chemistry = d.battery_chemistry.wire_str();
for metal in battery_recycled_chemistry_conflicts(
chemistry,
d.recycled_content_cobalt_pct,
d.recycled_content_lithium_pct,
d.recycled_content_nickel_pct,
d.recycled_content_lead_pct,
) {
let field = match metal {
"cobalt" => "/recycledContentCobaltPct",
"lithium" => "/recycledContentLithiumPct",
"nickel" => "/recycledContentNickelPct",
"lead" => "/recycledContentLeadPct",
_ => "/recycledContent",
};
errors.push(FieldError {
field: field.to_owned(),
message: format!(
"{metal} recycled content declared for a {chemistry} battery, \
which contains no {metal}"
),
});
}
}
ProductGroupData::Textile(d) => {
if let Err(msg) = validate_fibre_composition(&d.fibre_composition) {
errors.push(FieldError {
field: "/fibreComposition".to_owned(),
message: msg,
});
}
push_svhc(d.svhc_substances.as_deref(), errors);
if let Some(ds) = d.durability_score
&& !(0.0..=10.0).contains(&ds)
{
errors.push(FieldError {
field: "/durabilityScore".to_owned(),
message: format!("durability_score {ds} must be 0.0–10.0"),
});
}
}
ProductGroupData::Electronics(d) => push_svhc(d.svhc_substances.as_deref(), errors),
ProductGroupData::Toy(d) => push_svhc(d.svhc_substances.as_deref(), errors),
ProductGroupData::Furniture(d) => push_svhc(d.svhc_substances.as_deref(), errors),
ProductGroupData::Mattress(d) => push_svhc(d.svhc_substances.as_deref(), errors),
ProductGroupData::Detergent(d) => {
if let Err(msg) = validate_surfactants(&d.surfactants) {
errors.push(FieldError {
field: "/surfactants".to_owned(),
message: msg,
});
}
}
_ => {}
}
}
fn push_svhc(substances: Option<&[SvhcSubstance]>, errors: &mut Vec<FieldError>) {
if let Some(s) = substances
&& let Err(msg) = validate_svhc_substances(s)
{
errors.push(FieldError {
field: "/svhcSubstances".to_owned(),
message: msg,
});
}
}
#[must_use = "a validation result that is discarded has validated nothing"]
pub fn validate_passport(passport: &Passport) -> Result<(), DppError> {
let mut errors = match passport.validate() {
Ok(()) => Vec::new(),
Err(DppError::Validation(ve)) => ve.errors,
Err(other) => return Err(other),
};
if let Some(ref data) = passport.product_group_data
&& let Err(ve) = validate_product_group_data(data)
{
errors.extend(ve.errors);
}
if errors.is_empty() {
Ok(())
} else {
Err(DppError::Validation(ValidationErrors { errors }))
}
}