mod generated;
pub use generated::*;
pub mod serde_flex {
use serde::de::{self, SeqAccess, Visitor};
use serde::Deserializer;
use std::fmt;
pub fn flex_string_opt<'de, D>(d: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
struct FlexStringVisitor;
impl<'de> Visitor<'de> for FlexStringVisitor {
type Value = Option<String>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a string, an array of strings, or null")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
if v.is_empty() { Ok(None) } else { Ok(Some(v.to_string())) }
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
if v.is_empty() { Ok(None) } else { Ok(Some(v)) }
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut parts = Vec::new();
while let Some(s) = seq.next_element::<String>()? {
if !s.is_empty() {
parts.push(s);
}
}
if parts.is_empty() { Ok(None) } else { Ok(Some(parts.join("\n"))) }
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> { Ok(None) }
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> { Ok(None) }
}
d.deserialize_any(FlexStringVisitor)
}
pub fn flex_vec_string_opt<'de, D>(d: D) -> Result<Option<Vec<String>>, D::Error>
where
D: Deserializer<'de>,
{
struct FlexVecVisitor;
impl<'de> Visitor<'de> for FlexVecVisitor {
type Value = Option<Vec<String>>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a string, an array of strings, or null")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
if v.is_empty() {
Ok(None)
} else {
Ok(Some(vec![v.to_string()]))
}
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
if v.is_empty() {
Ok(None)
} else {
Ok(Some(vec![v]))
}
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut items = Vec::new();
while let Some(s) = seq.next_element::<String>()? {
if !s.is_empty() {
items.push(s);
}
}
if items.is_empty() {
Ok(None)
} else {
Ok(Some(items))
}
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
d.deserialize_any(FlexVecVisitor)
}
pub fn flex_vec_string_or_obj_opt<'de, D>(d: D) -> Result<Option<Vec<String>>, D::Error>
where
D: Deserializer<'de>,
{
use serde::Deserialize;
let v = Option::<serde_json::Value>::deserialize(d)
.map_err(de::Error::custom)?;
fn obj_to_string(obj: serde_json::Value) -> String {
if let serde_json::Value::Object(ref map) = obj {
if let Some(ai) = map.get("AdditionalInfo") {
if let Some(ft) = ai.get("FullText") {
let parts: Vec<String> = match ft {
serde_json::Value::Array(arr) => arr
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect(),
serde_json::Value::String(s) => vec![s.clone()],
_ => vec![],
};
if !parts.is_empty() {
return parts.join(" / ");
}
}
}
if let Some(serde_json::Value::String(name)) = map.get("ItemName") {
if !name.is_empty() {
return name.clone();
}
}
}
serde_json::to_string(&obj).unwrap_or_default()
}
match v {
None => Ok(None),
Some(serde_json::Value::String(s)) => {
if s.is_empty() { Ok(None) } else { Ok(Some(vec![s])) }
}
Some(serde_json::Value::Array(arr)) => {
let items: Vec<String> = arr
.into_iter()
.filter_map(|elem| match elem {
serde_json::Value::String(s) if !s.is_empty() => Some(s),
serde_json::Value::Object(_) => {
let s = obj_to_string(elem);
if s.is_empty() { None } else { Some(s) }
}
_ => None,
})
.collect();
if items.is_empty() { Ok(None) } else { Ok(Some(items)) }
}
_ => Ok(None),
}
}
macro_rules! flex_single_or_vec_opt {
($fn_name:ident, $T:ty) => {
pub fn $fn_name<'de, D>(d: D) -> Result<Option<Vec<$T>>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let v = Option::<serde_json::Value>::deserialize(d)
.map_err(serde::de::Error::custom)?;
match v {
None => Ok(None),
Some(serde_json::Value::Array(arr)) => {
let items: Result<Vec<$T>, _> = arr
.into_iter()
.map(|val| {
serde_json::from_value(val).map_err(serde::de::Error::custom)
})
.collect();
Ok(Some(items?))
}
Some(obj) => {
let item = serde_json::from_value::<$T>(obj)
.map_err(serde::de::Error::custom)?;
Ok(Some(vec![item]))
}
}
}
};
}
use crate::schema::{
HandlingAndStorageStorageConditionsForSafeStorage,
HazardIdentificationClassificationHealthEffectSpecificTargetOrganRE,
HazardIdentificationClassificationHealthEffectSpecificTargetOrganSE,
};
flex_single_or_vec_opt!(
flex_stoase_opt,
HazardIdentificationClassificationHealthEffectSpecificTargetOrganSE
);
flex_single_or_vec_opt!(
flex_stoare_opt,
HazardIdentificationClassificationHealthEffectSpecificTargetOrganRE
);
pub fn flex_storage_conditions_opt<'de, D>(
d: D,
) -> Result<Option<HandlingAndStorageStorageConditionsForSafeStorage>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let v = Option::<serde_json::Value>::deserialize(d)
.map_err(serde::de::Error::custom)?;
match v {
None => Ok(None),
Some(serde_json::Value::String(s)) if s.is_empty() => Ok(None),
Some(serde_json::Value::String(s)) => {
Ok(Some(HandlingAndStorageStorageConditionsForSafeStorage {
technical_measures_and_storage_conditions: Some(s),
..Default::default()
}))
}
Some(obj) => serde_json::from_value(obj)
.map_err(serde::de::Error::custom),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fire_fighting_full_text_accepts_array() {
let json = r#"{
"FireFightingMeasures": {
"FireAndExplosionHazards": {
"FullText": ["有害燃焼副産物:ケイ素酸化物", "炭素酸化物"]
},
"FireFightingProcedures": {
"FullText": "現場の状況に応じて適切な消火手段を用いる。"
}
}
}"#;
let sds: SdsRoot = serde_json::from_str(json)
.expect("FireFightingMeasures with array FullText should deserialize");
let ffm = sds.fire_fighting_measures.as_ref().expect("FireFightingMeasures must be Some");
let hazards = ffm.fire_and_explosion_hazards.as_ref().expect("FireAndExplosionHazards must be Some");
let full_text = hazards.full_text.as_deref().expect("FullText must be Some");
assert!(full_text.contains("有害燃焼副産物"), "FullText content lost: {full_text}");
assert!(full_text.contains("炭素酸化物"), "second element lost: {full_text}");
}
#[test]
fn stability_reactivity_substance_accepts_array() {
let json = r#"{
"StabilityReactivity": {
"HazardousDecompositionProducts": {
"Substance": ["二酸化炭素(CO2)", "アンモニア", "窒素酸化物(NOx)"]
}
}
}"#;
let sds: SdsRoot = serde_json::from_str(json)
.expect("StabilityReactivity with array Substance should deserialize");
let sr = sds.stability_reactivity.as_ref().expect("StabilityReactivity must be Some");
let hdp = sr.hazardous_decomposition_products.as_ref().expect("HazardousDecompositionProducts must be Some");
let substance = hdp.substance.as_deref().expect("Substance must be Some");
assert!(substance.contains("二酸化炭素"), "first element lost: {substance}");
assert!(substance.contains("アンモニア"), "second element lost: {substance}");
assert!(substance.contains("窒素酸化物"), "third element lost: {substance}");
}
#[test]
fn sds_root_round_trip_empty() {
let sds = SdsRoot::default();
let json = serde_json::to_string(&sds).unwrap();
assert_eq!(json, "{}");
let sds2: SdsRoot = serde_json::from_str("{}").unwrap();
let json2 = serde_json::to_string(&sds2).unwrap();
assert_eq!(json2, "{}");
}
#[test]
fn sds_root_round_trip_partial() {
let json = r#"{"Datasheet":{"IssueDate":"2024-03-31","SDS-SchemaVersionNo":"1.0"}}"#;
let sds: SdsRoot = serde_json::from_str(json).unwrap();
assert_eq!(sds.datasheet.as_ref().unwrap().issue_date.as_deref(), Some("2024-03-31"));
let out = serde_json::to_string(&sds).unwrap();
let v1: serde_json::Value = serde_json::from_str(json).unwrap();
let v2: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v1, v2);
}
}