use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer};
fn is_null_sentinel(s: &str) -> bool {
matches!(s.trim(), "" | "-" | "--" | "N/A" | "n/a")
}
fn parse_flex_date(s: &str) -> Result<NaiveDate, chrono::ParseError> {
let date_part = s.split([';', ' ', 'T', ',']).next().unwrap_or(s).trim();
if let Ok(date) = NaiveDate::parse_from_str(date_part, "%Y-%m-%d") {
return Ok(date);
}
NaiveDate::parse_from_str(date_part, "%Y%m%d")
}
pub fn deserialize_flex_date<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse_flex_date(&s).map_err(serde::de::Error::custom)
}
pub fn deserialize_optional_decimal<'de, D>(deserializer: D) -> Result<Option<Decimal>, D::Error>
where
D: Deserializer<'de>,
{
let s = Option::<String>::deserialize(deserializer)?;
match s.as_deref() {
None => Ok(None),
Some(s) if is_null_sentinel(s) => Ok(None),
Some(s) => {
let cleaned = s.trim().replace(',', "");
cleaned
.parse::<Decimal>()
.map(Some)
.map_err(serde::de::Error::custom)
}
}
}
pub fn deserialize_optional_date<'de, D>(deserializer: D) -> Result<Option<NaiveDate>, D::Error>
where
D: Deserializer<'de>,
{
let s = Option::<String>::deserialize(deserializer)?;
match s.as_deref() {
None => Ok(None),
Some(s) if is_null_sentinel(s) => Ok(None),
Some(s) if s.eq_ignore_ascii_case("MULTI") => Ok(None),
Some(s) => parse_flex_date(s)
.map(Some)
.map_err(serde::de::Error::custom),
}
}
pub fn deserialize_optional_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let s = Option::<String>::deserialize(deserializer)?;
match s.as_deref() {
None => Ok(None),
Some(s) if is_null_sentinel(s) => Ok(None),
Some(_) => Ok(s),
}
}
pub fn deserialize_optional_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
where
D: Deserializer<'de>,
{
let s = Option::<String>::deserialize(deserializer)?;
match s.as_deref() {
None => Ok(None),
Some(s) if is_null_sentinel(s) => Ok(None),
Some(v) if v.eq_ignore_ascii_case("y") || v.eq_ignore_ascii_case("yes") => Ok(Some(true)),
Some(v) if v.eq_ignore_ascii_case("n") || v.eq_ignore_ascii_case("no") => Ok(Some(false)),
Some(other) => Err(serde::de::Error::custom(format!(
"Invalid boolean value '{}', expected 'Y'/'N' or 'Yes'/'No'",
other
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct TestStruct {
#[serde(
rename = "@value",
default,
deserialize_with = "deserialize_optional_decimal"
)]
value: Option<Decimal>,
#[serde(
rename = "@date",
default,
deserialize_with = "deserialize_optional_date"
)]
date: Option<NaiveDate>,
#[serde(
rename = "@text",
default,
deserialize_with = "deserialize_optional_string"
)]
text: Option<String>,
#[serde(
rename = "@flag",
default,
deserialize_with = "deserialize_optional_bool"
)]
flag: Option<bool>,
}
#[test]
fn test_empty_string_decimal() {
let xml = r#"<TestStruct value="" />"#;
let result: Result<TestStruct, _> = quick_xml::de::from_str(xml);
assert!(result.is_ok());
assert_eq!(result.unwrap().value, None);
}
#[test]
fn test_valid_decimal() {
let xml = r#"<TestStruct value="123.45" />"#;
let result: Result<TestStruct, _> = quick_xml::de::from_str(xml);
assert!(result.is_ok());
assert_eq!(result.unwrap().value, Some(Decimal::new(12345, 2)));
}
#[test]
fn test_empty_string_date() {
let xml = r#"<TestStruct date="" />"#;
let result: Result<TestStruct, _> = quick_xml::de::from_str(xml);
assert!(result.is_ok());
assert_eq!(result.unwrap().date, None);
}
#[test]
fn test_multi_sentinel_date() {
let xml = r#"<TestStruct date="MULTI" />"#;
let result: Result<TestStruct, _> = quick_xml::de::from_str(xml);
assert!(result.is_ok());
assert_eq!(result.unwrap().date, None);
}
#[test]
fn test_empty_string_text() {
let xml = r#"<TestStruct text="" />"#;
let result: Result<TestStruct, _> = quick_xml::de::from_str(xml);
assert!(result.is_ok());
assert_eq!(result.unwrap().text, None);
}
#[test]
fn test_bool_y() {
let xml = r#"<TestStruct flag="Y" />"#;
let result: Result<TestStruct, _> = quick_xml::de::from_str(xml);
assert!(result.is_ok());
assert_eq!(result.unwrap().flag, Some(true));
}
#[test]
fn test_bool_n() {
let xml = r#"<TestStruct flag="N" />"#;
let result: Result<TestStruct, _> = quick_xml::de::from_str(xml);
assert!(result.is_ok());
assert_eq!(result.unwrap().flag, Some(false));
}
#[test]
fn test_bool_empty() {
let xml = r#"<TestStruct flag="" />"#;
let result: Result<TestStruct, _> = quick_xml::de::from_str(xml);
assert!(result.is_ok());
assert_eq!(result.unwrap().flag, None);
}
#[test]
fn test_bool_missing() {
let xml = r#"<TestStruct />"#;
let result: Result<TestStruct, _> = quick_xml::de::from_str(xml);
assert!(result.is_ok());
assert_eq!(result.unwrap().flag, None);
}
#[test]
fn test_bool_invalid() {
let xml = r#"<TestStruct flag="X" />"#;
let result: Result<TestStruct, _> = quick_xml::de::from_str(xml);
assert!(result.is_err());
}
#[test]
fn test_decimal_thousands_separator() {
let xml = r#"<TestStruct value="2,345,678.99" />"#;
let result: TestStruct = quick_xml::de::from_str(xml).unwrap();
assert_eq!(result.value, Some(Decimal::new(234567899, 2)));
}
#[test]
fn test_decimal_dash_sentinels() {
for v in ["-", "--", "N/A", "n/a"] {
let xml = format!(r#"<TestStruct value="{v}" />"#);
let result: TestStruct = quick_xml::de::from_str(&xml).unwrap();
assert_eq!(result.value, None, "sentinel {v:?} should parse to None");
}
}
#[test]
fn test_string_sentinels_to_none() {
let xml = r#"<TestStruct text="N/A" />"#;
let result: TestStruct = quick_xml::de::from_str(xml).unwrap();
assert_eq!(result.text, None);
}
#[test]
fn test_date_with_time_component() {
let xml = r#"<TestStruct date="20260528;170000" />"#;
let result: TestStruct = quick_xml::de::from_str(xml).unwrap();
assert_eq!(result.date, NaiveDate::from_ymd_opt(2026, 5, 28));
}
#[test]
fn test_bool_yes_no() {
let yes: TestStruct = quick_xml::de::from_str(r#"<TestStruct flag="Yes" />"#).unwrap();
assert_eq!(yes.flag, Some(true));
let no: TestStruct = quick_xml::de::from_str(r#"<TestStruct flag="No" />"#).unwrap();
assert_eq!(no.flag, Some(false));
}
#[test]
fn test_bool_dash_sentinel() {
let xml = r#"<TestStruct flag="--" />"#;
let result: TestStruct = quick_xml::de::from_str(xml).unwrap();
assert_eq!(result.flag, None);
}
}