use serde::{Deserialize, Serialize};
use super::provenance::{EvidenceLevel, MeasurementConditions};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceApplicability {
FinishedProduct,
SameBatch,
EquivalentBatch,
Component,
ReferenceSubstance,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EvidenceSource {
pub id: String,
pub level: EvidenceLevel,
pub reference: String,
pub issuer: Option<String>,
pub document_date: Option<String>,
pub applies_to: EvidenceApplicability,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MeasuredValueEvidence {
pub value: f64,
pub unit: String,
pub method: Option<String>,
pub conditions: MeasurementConditions,
pub sample_id: Option<String>,
pub batch_id: Option<String>,
pub evidence_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExplosiveLimitsEvidence {
pub lower: Option<f64>,
pub upper: Option<f64>,
pub unit: String,
pub method: Option<String>,
pub conditions: MeasurementConditions,
pub sample_id: Option<String>,
pub batch_id: Option<String>,
pub evidence_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TestResultEvidence {
pub result: String,
pub method: Option<String>,
pub sample_id: Option<String>,
pub batch_id: Option<String>,
pub evidence_id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct MeasuredPropertiesInput {
pub flash_point: Vec<MeasuredValueEvidence>,
pub boiling_point: Vec<MeasuredValueEvidence>,
pub vapor_pressure: Vec<MeasuredValueEvidence>,
pub explosive_limits: Vec<ExplosiveLimitsEvidence>,
pub self_reactivity: Vec<TestResultEvidence>,
pub oxidizing_properties: Vec<TestResultEvidence>,
pub metal_corrosivity: Vec<TestResultEvidence>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn measured_properties_input_defaults_to_all_empty() {
let input = MeasuredPropertiesInput::default();
assert!(input.flash_point.is_empty());
assert!(input.boiling_point.is_empty());
assert!(input.vapor_pressure.is_empty());
assert!(input.explosive_limits.is_empty());
assert!(input.self_reactivity.is_empty());
assert!(input.oxidizing_properties.is_empty());
assert!(input.metal_corrosivity.is_empty());
}
#[test]
fn evidence_applicability_serializes_snake_case() {
let json = serde_json::to_string(&EvidenceApplicability::EquivalentBatch).unwrap();
assert_eq!(json, "\"equivalent_batch\"");
}
#[test]
fn missing_measured_properties_key_becomes_all_empty_collections() {
let input: MeasuredPropertiesInput = serde_json::from_str("{}").unwrap();
assert!(input.flash_point.is_empty());
assert!(input.boiling_point.is_empty());
assert!(input.vapor_pressure.is_empty());
assert!(input.explosive_limits.is_empty());
assert!(input.self_reactivity.is_empty());
assert!(input.oxidizing_properties.is_empty());
assert!(input.metal_corrosivity.is_empty());
}
#[test]
fn partial_measured_properties_object_may_contain_only_flash_point() {
let json = r#"{
"flash_point": [
{"value": 12.0, "unit": "degC", "method": null,
"conditions": {"temperature_c": 20.0, "pressure_kpa": null, "atmosphere": null},
"sample_id": null, "batch_id": "BATCH-001", "evidence_id": "flash-point-report"}
]
}"#;
let input: MeasuredPropertiesInput = serde_json::from_str(json).unwrap();
assert_eq!(input.flash_point.len(), 1);
assert!(input.boiling_point.is_empty());
assert!(input.vapor_pressure.is_empty());
assert!(input.explosive_limits.is_empty());
assert!(input.self_reactivity.is_empty());
assert!(input.oxidizing_properties.is_empty());
assert!(input.metal_corrosivity.is_empty());
}
#[test]
fn unknown_measured_property_name_fails() {
let json = r#"{"flash_points": []}"#;
assert!(serde_json::from_str::<MeasuredPropertiesInput>(json).is_err());
}
#[test]
fn missing_evidence_id_fails() {
let json = r#"{"level":"product_test_report","reference":"ref",
"issuer":null,"document_date":null,"applies_to":"finished_product"}"#;
assert!(serde_json::from_str::<EvidenceSource>(json).is_err());
}
#[test]
fn missing_evidence_reference_fails() {
let json = r#"{"id":"ev1","level":"product_test_report",
"issuer":null,"document_date":null,"applies_to":"finished_product"}"#;
assert!(serde_json::from_str::<EvidenceSource>(json).is_err());
}
#[test]
fn missing_measured_value_evidence_id_fails() {
let json = r#"{"value":1.0,"unit":"degC","method":null,
"conditions":{"temperature_c":20.0,"pressure_kpa":null,"atmosphere":null},
"sample_id":null,"batch_id":null}"#;
assert!(serde_json::from_str::<MeasuredValueEvidence>(json).is_err());
}
#[test]
fn missing_required_conditions_still_fails() {
let json = r#"{"value":1.0,"unit":"degC","method":null,
"sample_id":null,"batch_id":null,"evidence_id":"ev1"}"#;
assert!(serde_json::from_str::<MeasuredValueEvidence>(json).is_err());
}
}