Skip to content

Commit

Permalink
Introduce empty_as_none and empty_as_none_vec deserializers
Browse files Browse the repository at this point in the history
This commit changes the way `foo=` is deserialized into `Option<_>`. Previously it was deserialized as `None`, now it will be deserialized as `Some(_)`, unless the new `empty_as_none` deserializer is used via `#[serde(deserialize_with = "crate::de::empty_as_none")]`. This allows users to decide on the deserialization strategy on a field-by-field basis.
  • Loading branch information
Turbo87 committed Dec 18, 2024
1 parent 6ff055a commit 8960c9d
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ struct Form {
// `Option`-typed fields (except when combined with some other attributes).
single: Option<u32>,
// Not using `serde(default)` here to require at least one occurrence.
#[serde(deserialize_with = "serde_html_form::de::empty_as_none_vec")]
at_least_one: Vec<Option<u32>>,
}

Expand Down
41 changes: 41 additions & 0 deletions src/de.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Deserialization support for the `application/x-www-form-urlencoded` format.
use std::borrow::Cow;
use std::io::Read;

use form_urlencoded::{parse, Parse as UrlEncodedParse};
Expand Down Expand Up @@ -178,6 +179,46 @@ impl<'de> de::Deserializer<'de> for Deserializer<'de> {
}
}

pub fn empty_as_none<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: de::Deserialize<'de>,
D: serde::Deserializer<'de>,
{
use de::Deserialize;

let cow = Cow::<'de, str>::deserialize(deserializer)?;
empty_as_none_cow(cow).map_err(de::Error::custom)
}

pub fn empty_as_none_vec<'de, T, D>(deserializer: D) -> Result<Vec<Option<T>>, D::Error>
where
T: de::Deserialize<'de>,
D: serde::Deserializer<'de>,
{
use de::Deserialize;

Vec::<Cow<'de, str>>::deserialize(deserializer)?
.into_iter()
.map(empty_as_none_cow)
.collect::<Result<Vec<_>, _>>()
.map_err(de::Error::custom)
}

fn empty_as_none_cow<'de, T>(
cow: Cow<'de, str>,
) -> Result<Option<T>, <Part<'_> as de::Deserializer<'de>>::Error>
where
T: de::Deserialize<'de>,
{
use de::IntoDeserializer;

if cow.is_empty() {
Ok(None)
} else {
Ok(Some(T::deserialize(Part(cow).into_deserializer())?))
}
}

struct PartIterator<'de>(UrlEncodedParse<'de>);

impl<'de> Iterator for PartIterator<'de> {
Expand Down
6 changes: 1 addition & 5 deletions src/de/part.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,7 @@ impl<'de> de::Deserializer<'de> for Part<'de> {
where
V: de::Visitor<'de>,
{
if self.0.is_empty() {
visitor.visit_none()
} else {
visitor.visit_some(self)
}
visitor.visit_some(self)
}

fn deserialize_enum<V>(
Expand Down
35 changes: 35 additions & 0 deletions src/de/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ fn deserialize_option_vec_int() {
fn deserialize_option_no_value() {
#[derive(Deserialize, PartialEq, Debug)]
struct Form {
#[serde(deserialize_with = "crate::de::empty_as_none")]
value: Option<f64>,
}

Expand All @@ -125,6 +126,7 @@ fn deserialize_option_no_value() {
fn deserialize_vec_options_no_value() {
#[derive(Deserialize, PartialEq, Debug)]
struct Form {
#[serde(deserialize_with = "crate::de::empty_as_none_vec")]
value: Vec<Option<f64>>,
}

Expand All @@ -135,6 +137,7 @@ fn deserialize_vec_options_no_value() {
fn deserialize_vec_options_some_values() {
#[derive(Deserialize, PartialEq, Debug)]
struct Form {
#[serde(deserialize_with = "crate::de::empty_as_none_vec")]
value: Vec<Option<f64>>,
}

Expand Down Expand Up @@ -210,3 +213,35 @@ fn deserialize_unit_enum() {
fn deserialize_unit_type() {
assert_eq!(super::from_str(""), Ok(()));
}

#[test]
fn issue_13() {
#[derive(Deserialize, PartialEq, Debug)]
struct FormA {
foo: Option<String>,
}

#[derive(Deserialize, PartialEq, Debug)]
struct FormB {
foo: Option<Vec<String>>,
}

#[derive(Deserialize, PartialEq, Debug)]
struct FormA2 {
#[serde(deserialize_with = "crate::de::empty_as_none")]
foo: Option<f64>,
}

#[derive(Deserialize, PartialEq, Debug)]
struct FormB2 {
foo: Option<Vec<f64>>,
}

assert_eq!(super::from_str("foo="), Ok(FormA { foo: Some("".to_owned()) }));
assert_eq!(super::from_str("foo="), Ok(FormB { foo: Some(vec!["".to_owned()]) }));
assert_eq!(super::from_str("foo="), Ok(FormA2 { foo: None }));
assert_eq!(
super::from_str::<FormB2>("foo=").unwrap_err().to_string(),
"cannot parse float from empty string"
);
}

0 comments on commit 8960c9d

Please sign in to comment.