sdsforge_core/generation/
evidence.rs1use serde::{Deserialize, Serialize};
2
3use super::provenance::{EvidenceLevel, MeasurementConditions};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum EvidenceApplicability {
12 FinishedProduct,
13 SameBatch,
14 EquivalentBatch,
15 Component,
16 ReferenceSubstance,
17 Unknown,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct EvidenceSource {
27 pub id: String,
29 pub level: EvidenceLevel,
30 pub reference: String,
31 pub issuer: Option<String>,
32 pub document_date: Option<String>,
33 pub applies_to: EvidenceApplicability,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(deny_unknown_fields)]
50pub struct MeasuredValueEvidence {
51 pub value: f64,
52 pub unit: String,
53 pub method: Option<String>,
54 pub conditions: MeasurementConditions,
55 pub sample_id: Option<String>,
56 pub batch_id: Option<String>,
57 pub evidence_id: String,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct ExplosiveLimitsEvidence {
66 pub lower: Option<f64>,
67 pub upper: Option<f64>,
68 pub unit: String,
69 pub method: Option<String>,
70 pub conditions: MeasurementConditions,
71 pub sample_id: Option<String>,
72 pub batch_id: Option<String>,
73 pub evidence_id: String,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct TestResultEvidence {
84 pub result: String,
85 pub method: Option<String>,
86 pub sample_id: Option<String>,
87 pub batch_id: Option<String>,
88 pub evidence_id: String,
89}
90
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
105#[serde(default, deny_unknown_fields)]
106pub struct MeasuredPropertiesInput {
107 pub flash_point: Vec<MeasuredValueEvidence>,
108 pub boiling_point: Vec<MeasuredValueEvidence>,
109 pub vapor_pressure: Vec<MeasuredValueEvidence>,
110 pub explosive_limits: Vec<ExplosiveLimitsEvidence>,
111 pub self_reactivity: Vec<TestResultEvidence>,
112 pub oxidizing_properties: Vec<TestResultEvidence>,
113 pub metal_corrosivity: Vec<TestResultEvidence>,
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn measured_properties_input_defaults_to_all_empty() {
122 let input = MeasuredPropertiesInput::default();
123 assert!(input.flash_point.is_empty());
124 assert!(input.boiling_point.is_empty());
125 assert!(input.vapor_pressure.is_empty());
126 assert!(input.explosive_limits.is_empty());
127 assert!(input.self_reactivity.is_empty());
128 assert!(input.oxidizing_properties.is_empty());
129 assert!(input.metal_corrosivity.is_empty());
130 }
131
132 #[test]
133 fn evidence_applicability_serializes_snake_case() {
134 let json = serde_json::to_string(&EvidenceApplicability::EquivalentBatch).unwrap();
135 assert_eq!(json, "\"equivalent_batch\"");
136 }
137
138 #[test]
139 fn missing_measured_properties_key_becomes_all_empty_collections() {
140 let input: MeasuredPropertiesInput = serde_json::from_str("{}").unwrap();
141 assert!(input.flash_point.is_empty());
142 assert!(input.boiling_point.is_empty());
143 assert!(input.vapor_pressure.is_empty());
144 assert!(input.explosive_limits.is_empty());
145 assert!(input.self_reactivity.is_empty());
146 assert!(input.oxidizing_properties.is_empty());
147 assert!(input.metal_corrosivity.is_empty());
148 }
149
150 #[test]
151 fn partial_measured_properties_object_may_contain_only_flash_point() {
152 let json = r#"{
153 "flash_point": [
154 {"value": 12.0, "unit": "degC", "method": null,
155 "conditions": {"temperature_c": 20.0, "pressure_kpa": null, "atmosphere": null},
156 "sample_id": null, "batch_id": "BATCH-001", "evidence_id": "flash-point-report"}
157 ]
158 }"#;
159 let input: MeasuredPropertiesInput = serde_json::from_str(json).unwrap();
160 assert_eq!(input.flash_point.len(), 1);
161 assert!(input.boiling_point.is_empty());
162 assert!(input.vapor_pressure.is_empty());
163 assert!(input.explosive_limits.is_empty());
164 assert!(input.self_reactivity.is_empty());
165 assert!(input.oxidizing_properties.is_empty());
166 assert!(input.metal_corrosivity.is_empty());
167 }
168
169 #[test]
170 fn unknown_measured_property_name_fails() {
171 let json = r#"{"flash_points": []}"#;
173 assert!(serde_json::from_str::<MeasuredPropertiesInput>(json).is_err());
174 }
175
176 #[test]
177 fn missing_evidence_id_fails() {
178 let json = r#"{"level":"product_test_report","reference":"ref",
179 "issuer":null,"document_date":null,"applies_to":"finished_product"}"#;
180 assert!(serde_json::from_str::<EvidenceSource>(json).is_err());
181 }
182
183 #[test]
184 fn missing_evidence_reference_fails() {
185 let json = r#"{"id":"ev1","level":"product_test_report",
186 "issuer":null,"document_date":null,"applies_to":"finished_product"}"#;
187 assert!(serde_json::from_str::<EvidenceSource>(json).is_err());
188 }
189
190 #[test]
191 fn missing_measured_value_evidence_id_fails() {
192 let json = r#"{"value":1.0,"unit":"degC","method":null,
193 "conditions":{"temperature_c":20.0,"pressure_kpa":null,"atmosphere":null},
194 "sample_id":null,"batch_id":null}"#;
195 assert!(serde_json::from_str::<MeasuredValueEvidence>(json).is_err());
196 }
197
198 #[test]
199 fn missing_required_conditions_still_fails() {
200 let json = r#"{"value":1.0,"unit":"degC","method":null,
204 "sample_id":null,"batch_id":null,"evidence_id":"ev1"}"#;
205 assert!(serde_json::from_str::<MeasuredValueEvidence>(json).is_err());
206 }
207}