use serde::Deserialize as _;
pub fn de_nonempty_string<'de, D>(d: D) -> std::result::Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(d)?;
if value.trim().is_empty() {
Err(serde::de::Error::custom("token cannot be empty"))
} else {
Ok(value)
}
}
pub fn de_opt_nonempty_string<'de, D>(d: D) -> std::result::Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<String>::deserialize(d)?
.map(|value| {
if value.trim().is_empty() {
Err(serde::de::Error::custom("token cannot be empty"))
} else {
Ok(value)
}
})
.transpose()
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, serde::Deserialize)]
struct Probe {
#[serde(deserialize_with = "de_nonempty_string")]
required: String,
#[serde(default, deserialize_with = "de_opt_nonempty_string")]
optional: Option<String>,
}
fn parse(json: &str) -> Result<Probe, serde_json::Error> {
serde_json::from_str(json)
}
#[test]
fn a_present_token_survives() {
let p = parse(r#"{"required":"tok","optional":"opt"}"#).unwrap();
assert_eq!(p.required, "tok");
assert_eq!(p.optional.as_deref(), Some("opt"));
}
#[test]
fn an_absent_optional_is_none_not_an_error() {
assert!(parse(r#"{"required":"tok"}"#).unwrap().optional.is_none());
}
#[test]
fn a_blank_token_is_rejected_in_either_position() {
for json in [
r#"{"required":""}"#,
r#"{"required":" "}"#,
r#"{"required":"tok","optional":""}"#,
r#"{"required":"tok","optional":"\t"}"#,
] {
let err = parse(json).unwrap_err();
assert!(
err.to_string().contains("token cannot be empty"),
"{json} -> {err}"
);
}
}
}