sdsforge_core/generation/
input.rs1use serde::{Deserialize, Serialize};
2
3use super::evidence::{EvidenceSource, MeasuredPropertiesInput};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct ConcentrationRange {
13 pub exact: Option<f64>,
14 pub lower: Option<f64>,
15 pub upper: Option<f64>,
16 pub unit: String,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct ComponentInput {
23 pub cas_number: Option<String>,
24 pub name: Option<String>,
25 pub concentration: ConcentrationRange,
26}
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
39#[serde(deny_unknown_fields)]
40pub struct SupplierInput {
41 pub company_name: String,
42 pub address: Option<String>,
43 pub phone: Option<String>,
44 pub email: Option<String>,
45}
46
47#[derive(Debug, Clone, Default, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct ProductInput {
61 pub trade_name: String,
62 #[serde(default)]
63 pub other_names: Vec<String>,
64 pub supplier: SupplierInput,
65 pub components: Vec<ComponentInput>,
66 #[serde(default)]
74 pub measured_properties: MeasuredPropertiesInput,
75 #[serde(default)]
80 pub evidence: Vec<EvidenceSource>,
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 const MINIMAL_JSON: &str = r#"{
88 "trade_name": "Minimal Product",
89 "supplier": {"company_name": "Acme"},
90 "components": [
91 {"concentration": {"exact": 100.0, "unit": "%"}}
92 ]
93 }"#;
94
95 const VERBOSE_EQUIVALENT_JSON: &str = r#"{
96 "trade_name": "Minimal Product",
97 "other_names": [],
98 "supplier": {"company_name": "Acme", "address": null, "phone": null, "email": null},
99 "components": [
100 {"cas_number": null, "name": null,
101 "concentration": {"exact": 100.0, "lower": null, "upper": null, "unit": "%"}}
102 ],
103 "measured_properties": {"flash_point": [], "boiling_point": [], "vapor_pressure": [],
104 "explosive_limits": [], "self_reactivity": [], "oxidizing_properties": [],
105 "metal_corrosivity": []},
106 "evidence": []
107 }"#;
108
109 #[test]
110 fn minimal_json_omits_other_names_measured_properties_and_evidence() {
111 let input: ProductInput = serde_json::from_str(MINIMAL_JSON).unwrap();
112 assert_eq!(input.trade_name, "Minimal Product");
113 assert!(input.other_names.is_empty());
114 assert!(input.evidence.is_empty());
115 assert!(input.measured_properties.flash_point.is_empty());
116 assert!(input.measured_properties.boiling_point.is_empty());
117 assert!(input.components[0].cas_number.is_none());
118 assert!(input.components[0].name.is_none());
119 }
120
121 #[test]
122 fn verbose_and_concise_product_input_deserialize_to_equivalent_values() {
123 let concise: ProductInput = serde_json::from_str(MINIMAL_JSON).unwrap();
124 let verbose: ProductInput = serde_json::from_str(VERBOSE_EQUIVALENT_JSON).unwrap();
125 assert_eq!(
126 serde_json::to_string(&concise).unwrap(),
127 serde_json::to_string(&verbose).unwrap()
128 );
129 }
130
131 #[test]
132 fn missing_trade_name_fails() {
133 let json = r#"{"supplier":{"company_name":"Acme"},"components":[]}"#;
134 assert!(serde_json::from_str::<ProductInput>(json).is_err());
135 }
136
137 #[test]
138 fn missing_supplier_fails() {
139 let json = r#"{"trade_name":"X","components":[]}"#;
140 assert!(serde_json::from_str::<ProductInput>(json).is_err());
141 }
142
143 #[test]
144 fn missing_supplier_company_name_fails() {
145 let json = r#"{"trade_name":"X","supplier":{},"components":[]}"#;
146 assert!(serde_json::from_str::<ProductInput>(json).is_err());
147 }
148
149 #[test]
150 fn missing_components_fails() {
151 let json = r#"{"trade_name":"X","supplier":{"company_name":"Acme"}}"#;
152 assert!(serde_json::from_str::<ProductInput>(json).is_err());
153 }
154
155 #[test]
156 fn missing_component_concentration_fails() {
157 let json = r#"{"cas_number":"7732-18-5","name":"Water"}"#;
158 assert!(serde_json::from_str::<ComponentInput>(json).is_err());
159 }
160
161 #[test]
162 fn missing_concentration_unit_fails() {
163 let json = r#"{"exact": 100.0}"#;
164 assert!(serde_json::from_str::<ConcentrationRange>(json).is_err());
165 }
166
167 #[test]
168 fn unknown_top_level_field_fails() {
169 let json = r#"{"trade_name":"X","supplier":{"company_name":"Acme"},
170 "components":[],"typo_field": 1}"#;
171 let err = serde_json::from_str::<ProductInput>(json).unwrap_err();
172 assert!(err.to_string().contains("typo_field"));
173 }
174
175 #[test]
176 fn unknown_component_field_fails() {
177 let json = r#"{"concentration":{"exact":1.0,"unit":"%"},"bogus":true}"#;
178 let err = serde_json::from_str::<ComponentInput>(json).unwrap_err();
179 assert!(err.to_string().contains("bogus"));
180 }
181
182 #[test]
183 fn misspelled_concentration_field_fails_instead_of_being_ignored() {
184 let json =
188 r#"{"cas_number":"7732-18-5","name":"Water","concentation":{"exact":1.0,"unit":"%"}}"#;
189 assert!(serde_json::from_str::<ComponentInput>(json).is_err());
190 }
191
192 #[test]
193 fn explicit_empty_trade_name_is_not_replaced_by_default() {
194 let json = r#"{"trade_name":"","supplier":{"company_name":"Acme"},"components":[]}"#;
195 let input: ProductInput = serde_json::from_str(json).unwrap();
196 assert_eq!(input.trade_name, "");
197 }
198
199 #[test]
200 fn explicit_empty_components_parses_distinctly_from_a_missing_components_key() {
201 let json = r#"{"trade_name":"X","supplier":{"company_name":"Acme"},"components":[]}"#;
207 let input: ProductInput = serde_json::from_str(json).unwrap();
208 assert!(input.components.is_empty());
209 }
210}