From ecb2b8d47492bf91a6e4f9dfee9aaf18c318c244 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Fri, 17 Mar 2023 17:25:35 -0400 Subject: [PATCH 01/22] export `as_opt_` helper functions --- src/de_impl_opt.rs | 407 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 2 files changed, 409 insertions(+) create mode 100644 src/de_impl_opt.rs diff --git a/src/de_impl_opt.rs b/src/de_impl_opt.rs new file mode 100644 index 0000000..a632a8a --- /dev/null +++ b/src/de_impl_opt.rs @@ -0,0 +1,407 @@ +use std::{f64, fmt}; + +use crate::de::{self, Deserializer}; + +/// De-serialize either a `null`, `str`, `i64`, `f64`, or `u64` +/// as a *signed* value. +/// +/// # Errors +/// Returns an error if a string is non-empty and not a valid numeric +/// value, or if the unsigned value `u64` *overflows* when converted +/// to `i64`. +/// +/// # Returns +/// The signed (`i64`) value of a string or number. +/// +pub fn as_opt_i64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(DeserializeOptionalI64WithVisitor) +} + +/// De-serialize either a `null`, `str`, `u64`, `f64`, or `i64` +/// as an *unsigned* value. +/// +/// # Errors +/// Returns an error if a string is non-empty and not a valid numeric +/// value, or if the signed value `i64` represents a *negative* number. +/// +/// # Returns +/// The unsigned (`u64`) value of a string or number. +/// +pub fn as_opt_u64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(DeserializeOptionalU64WithVisitor) +} + +/// De-serialize either a `null`, `str`, `f64`, `u64`, or `i64` +/// as a *float* value. +/// +/// # Errors +/// Returns an error if a string is non-empty and not a valid numeric value. +/// +/// # Returns +/// The floating point (`f64`) value of a string or number. +/// +pub fn as_opt_f64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(DeserializeOptionalF64WithVisitor) +} + +/// De-serialize either a `null`, `bool`, `str`, `u64`, or `f64` +/// as a *boolean* value. +/// +/// # Truthy String Values +/// > Note: the pattern matching is *case insensitive*, so `YES` or `yes` +/// > works just the same. +/// +/// These are the following "truthy" string values that result in a +/// boolean value of `true`: +/// +/// - `1` +/// - `OK` +/// - `ON` +/// - `T` +/// - `TRUE` +/// - `Y` +/// - `YES` +/// +/// # Errors +/// Returns an error if an unsigned `u64` or a float `f64` value is not +/// a *zero* or a *one*. +/// +/// # Returns +/// The boolean (`bool`) value of a string or number. +/// +pub fn as_opt_bool<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(DeserializeOptionalBoolWithVisitor) +} + +/// De-serialize either a `null`, `str`, `bool`, `i64`, `f64`, or `u64` +/// as an (owned) *string* value. +/// +/// # Returns +/// The owned `String` value of a string, boolean, or number. +/// +pub fn as_opt_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(DeserializeOptionalStringWithVisitor) +} + +/// TODO maybe update these definitions into a macro ..? + +struct DeserializeOptionalU64WithVisitor; + +impl<'de> de::Visitor<'de> for DeserializeOptionalU64WithVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("an unsigned integer or a string") + } + + fn visit_i64(self, v: i64) -> Result + where + E: de::Error, + { + match u64::try_from(v) { + Ok(v) => Ok(Some(v)), + Err(_) => Err(E::custom(format!( + "overflow: Unable to convert signed value `{v:?}` to u64" + ))), + } + } + + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + Ok(Some(v)) + } + + fn visit_f64(self, v: f64) -> Result + where + E: de::Error, + { + Ok(Some(v.round() as u64)) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if let Ok(n) = v.parse::() { + Ok(Some(n)) + } else if v.is_empty() { + Ok(None) + } else if let Ok(f) = v.parse::() { + Ok(Some(f.round() as u64)) + } else { + Ok(None) + } + } + + /// We encounter a `null` value; this default implementation returns an + /// `Option::None` value. + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(None) + } +} + +struct DeserializeOptionalI64WithVisitor; + +impl<'de> de::Visitor<'de> for DeserializeOptionalI64WithVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a signed integer or a string") + } + + fn visit_i64(self, v: i64) -> Result + where + E: de::Error, + { + Ok(Some(v)) + } + + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + match i64::try_from(v) { + Ok(v) => Ok(Some(v)), + Err(_) => Err(E::custom(format!( + "overflow: Unable to convert unsigned value `{v:?}` to i64" + ))), + } + } + + fn visit_f64(self, v: f64) -> Result + where + E: de::Error, + { + Ok(Some(v.round() as i64)) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if let Ok(n) = v.parse::() { + Ok(Some(n)) + } else if v.is_empty() { + Ok(None) + } else if let Ok(f) = v.parse::() { + Ok(Some(f.round() as i64)) + } else { + Ok(None) + } + } + + /// We encounter a `null` value; this default implementation returns an + /// `Option::None` value. + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(None) + } +} + +struct DeserializeOptionalF64WithVisitor; + +impl<'de> de::Visitor<'de> for DeserializeOptionalF64WithVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a float or a string") + } + + fn visit_i64(self, v: i64) -> Result + where + E: de::Error, + { + Ok(Some(v as f64)) + } + + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + Ok(Some(v as f64)) + } + + fn visit_f64(self, v: f64) -> Result + where + E: de::Error, + { + Ok(Some(v)) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(v.parse::().ok()) + } + + /// We encounter a `null` value; this default implementation returns an + /// `Option::None` value. + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(None) + } +} + +struct DeserializeOptionalBoolWithVisitor; + +impl<'de> de::Visitor<'de> for DeserializeOptionalBoolWithVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("an integer (0 or 1) or a string") + } + + fn visit_bool(self, v: bool) -> Result + where + E: de::Error, + { + Ok(Some(v)) + } + + fn visit_i64(self, _: i64) -> Result + where + E: de::Error, + { + // needs a zero or one, just return `None` here + Ok(None) + } + + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + match v { + 0 => Ok(Some(false)), + 1 => Ok(Some(true)), + // needs a zero or one, just return `None` here + _ => Ok(None), + } + } + + fn visit_f64(self, v: f64) -> Result + where + E: de::Error, + { + match v as u8 { + 0 => Ok(Some(false)), + 1 => Ok(Some(true)), + // needs a zero or one, just return `None` here + _ => Ok(None), + } + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + // First, try to match common true/false phrases *without* + // using `to_uppercase()`. This approach is likely more efficient. + match v { + "" => Ok(None), + "t" | "T" | "true" | "True" | "1" => Ok(Some(true)), + "f" | "F" | "false" | "False" | "0" => Ok(Some(false)), + other => { + // So from the above, we've already matched the following + // "truthy" phrases: ["T", "1"]. + // To be completely thorough, we also need to do a case- + // insensitive match on ["OK", "ON", "TRUE", "Y", "YES"]. + match other.to_uppercase().as_str() { + "OK" | "ON" | "TRUE" | "Y" | "YES" => Ok(Some(true)), + _ => Ok(Some(false)), + } + } + } + } + + /// We encounter a `null` value; this default implementation returns an + /// `Option::None` value. + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(None) + } +} + +struct DeserializeOptionalStringWithVisitor; + +impl<'de> de::Visitor<'de> for DeserializeOptionalStringWithVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a string, bool, or a number") + } + + fn visit_bool(self, v: bool) -> Result + where + E: de::Error, + { + Ok(Some(v.to_string())) + } + + fn visit_i64(self, v: i64) -> Result + where + E: de::Error, + { + Ok(Some(v.to_string())) + } + + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + Ok(Some(v.to_string())) + } + + fn visit_f64(self, v: f64) -> Result + where + E: de::Error, + { + Ok(Some(v.to_string())) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if v.is_empty() { + Ok(None) + } else { + Ok(Some(v.to_owned())) + } + } + + /// We encounter a `null` value; this default implementation returns an + /// `Option::None` value. + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(None) + } +} diff --git a/src/lib.rs b/src/lib.rs index 5a43e86..32e46cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,8 +89,10 @@ //! mod de_impl; +mod de_impl_opt; pub use de_impl::{as_bool, as_f64, as_i64, as_string, as_u64}; +pub use de_impl_opt::{as_opt_bool, as_opt_f64, as_opt_i64, as_opt_string, as_opt_u64}; #[doc(hidden)] pub use serde; #[doc(hidden)] From 44477586e7405dd667b4bf5c7d406560fd84668c Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Fri, 17 Mar 2023 17:34:36 -0400 Subject: [PATCH 02/22] use `try_from(v).ok()` instead --- src/de_impl_opt.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/de_impl_opt.rs b/src/de_impl_opt.rs index a632a8a..bb63d61 100644 --- a/src/de_impl_opt.rs +++ b/src/de_impl_opt.rs @@ -113,12 +113,7 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalU64WithVisitor { where E: de::Error, { - match u64::try_from(v) { - Ok(v) => Ok(Some(v)), - Err(_) => Err(E::custom(format!( - "overflow: Unable to convert signed value `{v:?}` to u64" - ))), - } + Ok(u64::try_from(v).ok()) } fn visit_u64(self, v: u64) -> Result @@ -180,12 +175,7 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalI64WithVisitor { where E: de::Error, { - match i64::try_from(v) { - Ok(v) => Ok(Some(v)), - Err(_) => Err(E::custom(format!( - "overflow: Unable to convert unsigned value `{v:?}` to i64" - ))), - } + Ok(i64::try_from(v).ok()) } fn visit_f64(self, v: f64) -> Result From 11d6ce73d473bdf98919508d10b58a7782642f31 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Fri, 17 Mar 2023 17:50:00 -0400 Subject: [PATCH 03/22] add `examples/as_opt_bool` --- examples/as_opt_bool.rs | 142 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 examples/as_opt_bool.rs diff --git a/examples/as_opt_bool.rs b/examples/as_opt_bool.rs new file mode 100644 index 0000000..fbf0866 --- /dev/null +++ b/examples/as_opt_bool.rs @@ -0,0 +1,142 @@ +#![deny(warnings)] +#![warn(rust_2018_idioms)] + +#[macro_use] +extern crate log; + +use serde::Deserialize; +use serde_this_or_that::as_opt_bool; + +#[derive(Clone, Debug, Deserialize)] +pub struct Msg { + #[serde(deserialize_with = "as_opt_bool")] + pub timestamp: Option, +} + +// A simple type alias so as to DRY. +type Result = std::result::Result>; + +fn main() -> Result<()> { + sensible_env_logger::init!(); + + trace!("With Empty String:"); + let data = r#" + { + "timestamp": "" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + + trace!("With Null: "); + let data = r#" + { + "timestamp": null + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + + trace!("With Zero (0):"); + let data = r#" + { + "timestamp": 0 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some(false)); + + trace!("With One (1):"); + + let data = r#" + { + "timestamp": 1 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some(true)); + + trace!("With String (truthy #1):"); + + let data = r#" + { + "timestamp": "tRuE" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some(true)); + + trace!("With String (truthy #2):"); + + let data = r#" + { + "timestamp": "Y" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some(true)); + + trace!("With String (falsy):"); + + let data = r#" + { + "timestamp": "nope!" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some(false)); + + trace!("With String (Invalid Numeric):"); + + let data = r#" + { + "timestamp": "123456789076543210" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some(false)); + + trace!("With U64:"); + + let data = r#" + { + "timestamp": 123456789076543210 + }"#; + + if serde_json::from_str::(data).is_ok() { + trace!("All Good on the Home-front!"); + } else { + error!(" ERROR! no error should have occurred."); + assert_eq!(0, 1, "failure!"); + }; + + trace!("With I64:"); + + let data = r#" + { + "timestamp": -123 + }"#; + + if serde_json::from_str::(data).is_ok() { + trace!("All Good on the Home-front!"); + } else { + error!(" ERROR! no error should have occurred."); + assert_eq!(0, 1, "failure!"); + }; + + Ok(()) +} From 287f96ff33e436cc24e59fa1110099e5c55adb52 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Sat, 18 Mar 2023 00:08:02 -0400 Subject: [PATCH 04/22] add/update examples --- examples/as_opt_bool.rs | 8 +-- examples/as_opt_f64.rs | 83 +++++++++++++++++++++++++++ examples/as_opt_i64.rs | 98 ++++++++++++++++++++++++++++++++ examples/as_opt_string.rs | 114 ++++++++++++++++++++++++++++++++++++++ examples/as_opt_u64.rs | 88 +++++++++++++++++++++++++++++ examples/demo.rs | 17 +++++- 6 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 examples/as_opt_f64.rs create mode 100644 examples/as_opt_i64.rs create mode 100644 examples/as_opt_string.rs create mode 100644 examples/as_opt_u64.rs diff --git a/examples/as_opt_bool.rs b/examples/as_opt_bool.rs index fbf0866..35ad51b 100644 --- a/examples/as_opt_bool.rs +++ b/examples/as_opt_bool.rs @@ -117,8 +117,8 @@ fn main() -> Result<()> { "timestamp": 123456789076543210 }"#; - if serde_json::from_str::(data).is_ok() { - trace!("All Good on the Home-front!"); + if matches!(serde_json::from_str::(data), Ok(m) if m.timestamp.is_none()) { + trace!(" None"); } else { error!(" ERROR! no error should have occurred."); assert_eq!(0, 1, "failure!"); @@ -131,8 +131,8 @@ fn main() -> Result<()> { "timestamp": -123 }"#; - if serde_json::from_str::(data).is_ok() { - trace!("All Good on the Home-front!"); + if matches!(serde_json::from_str::(data), Ok(m) if m.timestamp.is_none()) { + trace!(" None"); } else { error!(" ERROR! no error should have occurred."); assert_eq!(0, 1, "failure!"); diff --git a/examples/as_opt_f64.rs b/examples/as_opt_f64.rs new file mode 100644 index 0000000..015dc81 --- /dev/null +++ b/examples/as_opt_f64.rs @@ -0,0 +1,83 @@ +#![deny(warnings)] +#![warn(rust_2018_idioms)] + +#[macro_use] +extern crate log; + +use serde::Deserialize; +use serde_this_or_that::as_opt_f64; + +#[derive(Clone, Debug, Deserialize)] +pub struct Msg { + #[serde(deserialize_with = "as_opt_f64")] + pub timestamp: Option, +} + +// A simple type alias so as to DRY. +type Result = std::result::Result>; + +fn main() -> Result<()> { + sensible_env_logger::init!(); + + trace!("With Empty String:"); + let data = r#" + { + "timestamp": "" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + + trace!("With Null: "); + let data = r#" + { + "timestamp": null + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + + trace!("With F64:"); + + let data = r#" + { + "timestamp": 123456789.076543210 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + trace!(" {m:?}"); + + trace!("With String:"); + + let data = r#" + { + "timestamp": "123456789076543210" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + trace!(" {m:?}"); + + trace!("With U64:"); + + let data = r#" + { + "timestamp": 123456789076543210 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + trace!(" {m:?}"); + + trace!("With I64:"); + + let data = r#" + { + "timestamp": -123456789076543210 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + trace!(" {m:?}"); + + Ok(()) +} diff --git a/examples/as_opt_i64.rs b/examples/as_opt_i64.rs new file mode 100644 index 0000000..39b6772 --- /dev/null +++ b/examples/as_opt_i64.rs @@ -0,0 +1,98 @@ +#![deny(warnings)] +#![warn(rust_2018_idioms)] + +#[macro_use] +extern crate log; + +use serde::Deserialize; +use serde_this_or_that::as_opt_i64; + +#[derive(Clone, Debug, Deserialize)] +pub struct Msg { + #[serde(deserialize_with = "as_opt_i64")] + pub timestamp: Option, +} + +// A simple type alias so as to DRY. +type Result = std::result::Result>; + +fn main() -> Result<()> { + sensible_env_logger::init!(); + + trace!("With Empty String:"); + let data = r#" + { + "timestamp": "" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + + trace!("With Null: "); + let data = r#" + { + "timestamp": null + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + + trace!("With U64:"); + + let data = r#" + { + "timestamp": 123456789076543210 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + trace!(" {m:?}"); + + trace!("With String:"); + + let data = r#" + { + "timestamp": "123456789076543210" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + trace!(" {m:?}"); + + trace!("With I64:"); + + let data = r#" + { + "timestamp": -123456789076543210 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + trace!(" {m:?}"); + + trace!("With F64:"); + + let data = r#" + { + "timestamp": -0.5 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some(-1)); + + trace!("With U64 (Larger):"); + + let data = r#" + { + "timestamp": 12345678907654321000 + }"#; + + if matches!(serde_json::from_str::(data), Ok(m) if m.timestamp.is_none()) { + trace!(" None"); + } else { + error!(" ERROR! no error should have occurred."); + assert_eq!(0, 1, "failure!"); + }; + + Ok(()) +} diff --git a/examples/as_opt_string.rs b/examples/as_opt_string.rs new file mode 100644 index 0000000..a9e8c0b --- /dev/null +++ b/examples/as_opt_string.rs @@ -0,0 +1,114 @@ +#![deny(warnings)] +#![warn(rust_2018_idioms)] + +#[macro_use] +extern crate log; + +use serde::Deserialize; +use serde_this_or_that::as_opt_string; + +#[derive(Clone, Debug, Deserialize)] +pub struct Msg { + #[serde(deserialize_with = "as_opt_string")] + pub timestamp: Option, +} + +// A simple type alias so as to DRY. +type Result = std::result::Result>; + +fn main() -> Result<()> { + sensible_env_logger::init!(); + + trace!("With Empty String:"); + let data = r#" + { + "timestamp": "" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + + trace!("With Null: "); + let data = r#" + { + "timestamp": null + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + + trace!("With Zero (0):"); + let data = r#" + { + "timestamp": 0 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some("0".into())); + + trace!("With String:"); + + let data = r#" + { + "timestamp": "hello, world!" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some("hello, world!".into())); + + trace!("With Bool:"); + + let data = r#" + { + "timestamp": false + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some("false".into())); + + trace!("With U64:"); + + let data = r#" + { + "timestamp": 123456789076543210 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some("123456789076543210".into())); + + trace!("With I64:"); + + let data = r#" + { + "timestamp": -123 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some("-123".into())); + + trace!("With F64:"); + + let data = r#" + { + "timestamp": 1234567890.76543210 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some("1234567890.7654321".into())); + + Ok(()) +} diff --git a/examples/as_opt_u64.rs b/examples/as_opt_u64.rs new file mode 100644 index 0000000..6a3e042 --- /dev/null +++ b/examples/as_opt_u64.rs @@ -0,0 +1,88 @@ +#![deny(warnings)] +#![warn(rust_2018_idioms)] + +#[macro_use] +extern crate log; + +use serde::Deserialize; +use serde_this_or_that::as_opt_u64; + +#[derive(Clone, Debug, Deserialize)] +pub struct Msg { + #[serde(deserialize_with = "as_opt_u64")] + pub timestamp: Option, +} + +// A simple type alias so as to DRY. +type Result = std::result::Result>; + +fn main() -> Result<()> { + sensible_env_logger::init!(); + + trace!("With Empty String:"); + let data = r#" + { + "timestamp": "" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + + trace!("With Null: "); + let data = r#" + { + "timestamp": null + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + + trace!("With U64: "); + + let data = r#" + { + "timestamp": 123456789076543210 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + trace!(" {m:?}"); + + trace!("With F64:"); + + let data = r#" + { + "timestamp": 0.5 + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + trace!(" {m:?}"); + assert_eq!(m.timestamp, Some(1)); + + trace!("With String: "); + + let data = r#" + { + "timestamp": "123456789076543210" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + trace!(" {m:?}"); + + trace!("With I64: "); + + let data = r#" + { + "timestamp": -123 + }"#; + + if matches!(serde_json::from_str::(data), Ok(m) if m.timestamp.is_none()) { + trace!(" None"); + } else { + error!(" ERROR! no error should have occurred."); + assert_eq!(0, 1, "failure!"); + }; + + Ok(()) +} diff --git a/examples/demo.rs b/examples/demo.rs index 6089553..0edd90c 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -6,7 +6,7 @@ extern crate log; use serde::Deserialize; use serde_json::from_str; -use serde_this_or_that::{as_bool, as_f64, as_u64}; +use serde_this_or_that::{as_bool, as_f64, as_opt_i64, as_opt_string, as_u64}; #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -17,6 +17,10 @@ struct MyStruct { num_attempts: u64, #[serde(deserialize_with = "as_f64")] grade: f64, + #[serde(deserialize_with = "as_opt_string")] + notes: Option, + #[serde(default, deserialize_with = "as_opt_i64")] + confidence: Option, } fn main() -> Result<(), Box> { @@ -26,7 +30,8 @@ fn main() -> Result<(), Box> { { "isActive": "True", "numAttempts": "", - "grade": "81" + "grade": "81", + "notes": "" } "#; @@ -37,12 +42,16 @@ fn main() -> Result<(), Box> { assert!(s.is_active); assert_eq!(s.num_attempts, 0); assert_eq!(s.grade, 81.0); + assert_eq!(s.notes, None); + assert_eq!(s.confidence, None); let string = r#" { "isActive": false, "numAttempts": 1.7, - "grade": null + "grade": null, + "notes": true, + "confidence": "test!" } "#; @@ -53,6 +62,8 @@ fn main() -> Result<(), Box> { assert!(!s.is_active); assert_eq!(s.num_attempts, 2); assert_eq!(s.grade, 0.0); + assert_eq!(s.notes, Some("true".to_owned())); + assert_eq!(s.confidence, None); Ok(()) } From ebd49fcd26e4ac8ae3edad63fbc789002e8b2593 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Sat, 18 Mar 2023 01:17:42 -0400 Subject: [PATCH 05/22] add/update examples and docs --- README.md | 5 +++ examples/as_opt_bool.rs | 10 +++++ examples/as_opt_f64.rs | 10 +++++ examples/as_opt_i64.rs | 10 +++++ examples/as_opt_u64.rs | 10 +++++ src/de_impl.rs | 2 +- src/de_impl_opt.rs | 87 +++++++++++++++++++++++++++++++++-------- 7 files changed, 117 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 6a0246d..d75733f 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,11 @@ fn main() -> Result<(), Box> { - [`as_i64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_i64.html) - [`as_string`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_string.html) - [`as_u64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_u64.html) +- [`as_opt_bool`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_bool.html) +- [`as_opt_f64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_f64.html) +- [`as_opt_i64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_i64.html) +- [`as_opt_string`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_string.html) +- [`as_opt_u64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_u64.html) ## Examples diff --git a/examples/as_opt_bool.rs b/examples/as_opt_bool.rs index 35ad51b..6ab9192 100644 --- a/examples/as_opt_bool.rs +++ b/examples/as_opt_bool.rs @@ -25,6 +25,16 @@ fn main() -> Result<()> { "timestamp": "" }"#; + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, Some(false)); + trace!(" {m:?}"); + + trace!("With I64:"); + let data = r#" + { + "timestamp": -123 + }"#; + let m: Msg = serde_json::from_str(data).unwrap(); assert_eq!(m.timestamp, None); trace!(" {m:?}"); diff --git a/examples/as_opt_f64.rs b/examples/as_opt_f64.rs index 015dc81..015020c 100644 --- a/examples/as_opt_f64.rs +++ b/examples/as_opt_f64.rs @@ -29,6 +29,16 @@ fn main() -> Result<()> { assert_eq!(m.timestamp, None); trace!(" {m:?}"); + trace!("With Boolean:"); + let data = r#" + { + "timestamp": true + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + trace!("With Null: "); let data = r#" { diff --git a/examples/as_opt_i64.rs b/examples/as_opt_i64.rs index 39b6772..2694d6b 100644 --- a/examples/as_opt_i64.rs +++ b/examples/as_opt_i64.rs @@ -29,6 +29,16 @@ fn main() -> Result<()> { assert_eq!(m.timestamp, None); trace!(" {m:?}"); + trace!("With Boolean:"); + let data = r#" + { + "timestamp": true + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + trace!("With Null: "); let data = r#" { diff --git a/examples/as_opt_u64.rs b/examples/as_opt_u64.rs index 6a3e042..7fa3d14 100644 --- a/examples/as_opt_u64.rs +++ b/examples/as_opt_u64.rs @@ -29,6 +29,16 @@ fn main() -> Result<()> { assert_eq!(m.timestamp, None); trace!(" {m:?}"); + trace!("With Boolean:"); + let data = r#" + { + "timestamp": true + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + assert_eq!(m.timestamp, None); + trace!(" {m:?}"); + trace!("With Null: "); let data = r#" { diff --git a/src/de_impl.rs b/src/de_impl.rs index e2e7e94..9ac7d30 100644 --- a/src/de_impl.rs +++ b/src/de_impl.rs @@ -76,7 +76,7 @@ where /// a *zero* or a *one*. /// /// # Returns -/// The boolean (`bool`) value of a string or number. +/// The boolean (`bool`) value of a string, boolean, or number. /// pub fn as_bool<'de, D>(deserializer: D) -> Result where diff --git a/src/de_impl_opt.rs b/src/de_impl_opt.rs index bb63d61..02fe599 100644 --- a/src/de_impl_opt.rs +++ b/src/de_impl_opt.rs @@ -2,8 +2,9 @@ use std::{f64, fmt}; use crate::de::{self, Deserializer}; -/// De-serialize either a `null`, `str`, `i64`, `f64`, or `u64` -/// as a *signed* value. +/// De-serialize either a `str`, `i64`, `f64`, or `u64` +/// as a *signed* value wrapped in [`Some`], +/// and a `bool` or `null` value as [`None`]. /// /// # Errors /// Returns an error if a string is non-empty and not a valid numeric @@ -11,7 +12,13 @@ use crate::de::{self, Deserializer}; /// to `i64`. /// /// # Returns -/// The signed (`i64`) value of a string or number. +/// A [`Some`] with the signed (`i64`) value of a string +/// or number. +/// +/// A [`None`] in the case of: +/// * a `bool` value. +/// * a `null` value. +/// * any *de-serialization* errors. /// pub fn as_opt_i64<'de, D>(deserializer: D) -> Result, D::Error> where @@ -20,15 +27,22 @@ where deserializer.deserialize_any(DeserializeOptionalI64WithVisitor) } -/// De-serialize either a `null`, `str`, `u64`, `f64`, or `i64` -/// as an *unsigned* value. +/// De-serialize either a `str`, `u64`, `f64`, or `i64` +/// as an *unsigned* value wrapped in [`Some`], +/// and a `bool` or `null` value as [`None`]. /// /// # Errors /// Returns an error if a string is non-empty and not a valid numeric /// value, or if the signed value `i64` represents a *negative* number. /// /// # Returns -/// The unsigned (`u64`) value of a string or number. +/// A [`Some`] with the unsigned (`u64`) value of a string +/// or number. +/// +/// A [`None`] in the case of: +/// * a `bool` value. +/// * a `null` value. +/// * any *de-serialization* errors. /// pub fn as_opt_u64<'de, D>(deserializer: D) -> Result, D::Error> where @@ -37,14 +51,21 @@ where deserializer.deserialize_any(DeserializeOptionalU64WithVisitor) } -/// De-serialize either a `null`, `str`, `f64`, `u64`, or `i64` -/// as a *float* value. +/// De-serialize either a `str`, `f64`, `u64`, or `i64` +/// as a *float* value wrapped in [`Some`], +/// and a `bool` or `null` value as [`None`]. /// /// # Errors /// Returns an error if a string is non-empty and not a valid numeric value. /// /// # Returns -/// The floating point (`f64`) value of a string or number. +/// A [`Some`] with the floating point (`f64`) value of a string +/// or number. +/// +/// A [`None`] in the case of: +/// * a `bool` value. +/// * a `null` value. +/// * any *de-serialization* errors. /// pub fn as_opt_f64<'de, D>(deserializer: D) -> Result, D::Error> where @@ -53,8 +74,9 @@ where deserializer.deserialize_any(DeserializeOptionalF64WithVisitor) } -/// De-serialize either a `null`, `bool`, `str`, `u64`, or `f64` -/// as a *boolean* value. +/// De-serialize either a `bool`, `str`, `u64`, or `f64` +/// as a *boolean* value wrapped in [`Some`], +/// and an `i64` or `null` value as [`None`]. /// /// # Truthy String Values /// > Note: the pattern matching is *case insensitive*, so `YES` or `yes` @@ -76,7 +98,13 @@ where /// a *zero* or a *one*. /// /// # Returns -/// The boolean (`bool`) value of a string or number. +/// A [`Some`] with the boolean (`bool`) value of a string, +/// boolean, or number. +/// +/// A [`None`] in the case of: +/// * an `i64` value. +/// * a `null` value. +/// * any *de-serialization* errors. /// pub fn as_opt_bool<'de, D>(deserializer: D) -> Result, D::Error> where @@ -85,11 +113,18 @@ where deserializer.deserialize_any(DeserializeOptionalBoolWithVisitor) } -/// De-serialize either a `null`, `str`, `bool`, `i64`, `f64`, or `u64` -/// as an (owned) *string* value. +/// De-serialize either a `str`, `bool`, `i64`, `f64`, or `u64` +/// as an (owned) *string* value wrapped in [`Some`], +/// and an empty string or `null` value as [`None`]. /// /// # Returns -/// The owned `String` value of a string, boolean, or number. +/// A [`Some`] with the owned `String` value of a string, +/// boolean, or number. +/// +/// A [`None`] in the case of: +/// * a `null` value. +/// * an empty string. +/// * any *de-serialization* errors. /// pub fn as_opt_string<'de, D>(deserializer: D) -> Result, D::Error> where @@ -109,6 +144,13 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalU64WithVisitor { formatter.write_str("an unsigned integer or a string") } + fn visit_bool(self, _: bool) -> Result + where + E: de::Error, + { + Ok(None) + } + fn visit_i64(self, v: i64) -> Result where E: de::Error, @@ -164,6 +206,13 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalI64WithVisitor { formatter.write_str("a signed integer or a string") } + fn visit_bool(self, _: bool) -> Result + where + E: de::Error, + { + Ok(None) + } + fn visit_i64(self, v: i64) -> Result where E: de::Error, @@ -219,6 +268,13 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalF64WithVisitor { formatter.write_str("a float or a string") } + fn visit_bool(self, _: bool) -> Result + where + E: de::Error, + { + Ok(None) + } + fn visit_i64(self, v: i64) -> Result where E: de::Error, @@ -312,7 +368,6 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalBoolWithVisitor { // First, try to match common true/false phrases *without* // using `to_uppercase()`. This approach is likely more efficient. match v { - "" => Ok(None), "t" | "T" | "true" | "True" | "1" => Ok(Some(true)), "f" | "F" | "false" | "False" | "0" => Ok(Some(false)), other => { From ec558e054dc25860e8f499e080e1d8170e8a1052 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Sat, 18 Mar 2023 01:38:09 -0400 Subject: [PATCH 06/22] update usage in docs --- README.md | 9 +++++++-- src/lib.rs | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d75733f..22c6f10 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Here's an example of using `serde-this-or-that` in code: ```rust use serde::Deserialize; use serde_json::from_str; -use serde_this_or_that::{as_bool, as_f64, as_u64}; +use serde_this_or_that::{as_bool, as_f64, as_opt_i64, as_u64}; #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -38,6 +38,9 @@ struct MyStruct { num_attempts: u64, #[serde(deserialize_with = "as_f64")] grade: f64, + // uses #[serde(default)] in case the field is missing in JSON + #[serde(default, deserialize_with = "as_opt_i64")] + confidence: Option, } fn main() -> Result<(), Box> { @@ -45,7 +48,8 @@ fn main() -> Result<(), Box> { { "isActive": "True", "numAttempts": "", - "grade": "81" + "grade": "81", + "confidence": "A+" } "#; @@ -55,6 +59,7 @@ fn main() -> Result<(), Box> { assert!(s.is_active); assert_eq!(s.num_attempts, 0); assert_eq!(s.grade, 81.0); + assert_eq!(s.confidence, None); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 32e46cb..5dcc2e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ //! ```rust //! use serde::Deserialize; //! use serde_json::from_str; -//! use serde_this_or_that::{as_bool, as_f64, as_u64}; +//! use serde_this_or_that::{as_bool, as_f64, as_opt_i64, as_u64}; //! //! #[derive(Deserialize, Debug)] //! #[serde(rename_all = "camelCase")] @@ -30,6 +30,9 @@ //! num_attempts: u64, //! #[serde(deserialize_with = "as_f64")] //! grade: f64, +//! // uses #[serde(default)] in case the field is missing in JSON +//! #[serde(default, deserialize_with = "as_opt_i64")] +//! confidence: Option, //! } //! //! fn main() -> Result<(), Box> { @@ -37,7 +40,8 @@ //! { //! "isActive": "True", //! "numAttempts": "", -//! "grade": "81" +//! "grade": "81", +//! "confidence": "A+" //! } //! "#; //! @@ -47,6 +51,7 @@ //! assert!(s.is_active); //! assert_eq!(s.num_attempts, 0); //! assert_eq!(s.grade, 81.0); +//! assert_eq!(s.confidence, None); //! //! Ok(()) //! } From d7ac4fd0f9dff1b4100d5f0db7d7896c5df709f6 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Sat, 18 Mar 2023 02:01:39 -0400 Subject: [PATCH 07/22] docs: add section on "Optionals" --- README.md | 10 ++++++++++ src/lib.rs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/README.md b/README.md index 22c6f10..5152e3b 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,16 @@ folder, and can be run with `cargo bench`. [`DisplayFromStr`]: https://docs.rs/serde_with/latest/serde_with/struct.DisplayFromStr.html [`PickFirst`]: https://docs.rs/serde_with/latest/serde_with/struct.PickFirst.html +## Optionals + +The extra helper functions that begin with `as_opt`, return an `Option` of the respective data type `T`, +rather than the type `T` itself (see [#4](https://github.com/rnag/serde-this-or-that/issues/4)). + +On success, they return a value of `T` wrapped in [`Some`](https://doc.rust-lang.org/std/option/enum.Option.html#variant.Some). + +On error, or when there is a `null` value, or one of an *invalid* data type, the +`as_opt` helper functions return [`None`](https://doc.rust-lang.org/std/option/enum.Option.html#variant.None) instead. + ## Contributing Contributions are welcome! Open a pull request to fix a bug, or [open an issue][] diff --git a/src/lib.rs b/src/lib.rs index 5dcc2e5..24e1321 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,6 +83,16 @@ //! [`DisplayFromStr`]: https://docs.rs/serde_with/latest/serde_with/struct.DisplayFromStr.html //! [`PickFirst`]: https://docs.rs/serde_with/latest/serde_with/struct.PickFirst.html //! +//! ## Optionals +//! +//! The extra helper functions that begin with `as_opt`, return an `Option` of the respective data type `T`, +//! rather than the type `T` itself (see [#4](https://github.com/rnag/serde-this-or-that/issues/4)). +//! +//! On success, they return a value of `T` wrapped in [`Some`]. +//! +//! On error, or when there is a `null` value, or one of an *invalid* data type, the +//! `as_opt` helper functions return [`None`] instead. +//! //! //! ## Readme Docs //! From 419898f045909cb3b37dcd520651393aef5b8e07 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Sat, 18 Mar 2023 02:12:43 -0400 Subject: [PATCH 08/22] cleanup doc comments --- src/de_impl_opt.rs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/de_impl_opt.rs b/src/de_impl_opt.rs index 02fe599..e3c2c6a 100644 --- a/src/de_impl_opt.rs +++ b/src/de_impl_opt.rs @@ -6,11 +6,6 @@ use crate::de::{self, Deserializer}; /// as a *signed* value wrapped in [`Some`], /// and a `bool` or `null` value as [`None`]. /// -/// # Errors -/// Returns an error if a string is non-empty and not a valid numeric -/// value, or if the unsigned value `u64` *overflows* when converted -/// to `i64`. -/// /// # Returns /// A [`Some`] with the signed (`i64`) value of a string /// or number. @@ -19,6 +14,8 @@ use crate::de::{self, Deserializer}; /// * a `bool` value. /// * a `null` value. /// * any *de-serialization* errors. +/// * ex. a string is non-empty and not a valid numeric value. +/// * ex. the unsigned value `u64` *overflows* when converted to `i64`. /// pub fn as_opt_i64<'de, D>(deserializer: D) -> Result, D::Error> where @@ -31,10 +28,6 @@ where /// as an *unsigned* value wrapped in [`Some`], /// and a `bool` or `null` value as [`None`]. /// -/// # Errors -/// Returns an error if a string is non-empty and not a valid numeric -/// value, or if the signed value `i64` represents a *negative* number. -/// /// # Returns /// A [`Some`] with the unsigned (`u64`) value of a string /// or number. @@ -43,6 +36,8 @@ where /// * a `bool` value. /// * a `null` value. /// * any *de-serialization* errors. +/// * ex. a string is non-empty and not a valid numeric value. +/// * ex. the signed value `i64` represents a *negative* number. /// pub fn as_opt_u64<'de, D>(deserializer: D) -> Result, D::Error> where @@ -55,9 +50,6 @@ where /// as a *float* value wrapped in [`Some`], /// and a `bool` or `null` value as [`None`]. /// -/// # Errors -/// Returns an error if a string is non-empty and not a valid numeric value. -/// /// # Returns /// A [`Some`] with the floating point (`f64`) value of a string /// or number. @@ -66,6 +58,7 @@ where /// * a `bool` value. /// * a `null` value. /// * any *de-serialization* errors. +/// * ex. a string is non-empty and not a valid numeric value. /// pub fn as_opt_f64<'de, D>(deserializer: D) -> Result, D::Error> where @@ -93,10 +86,6 @@ where /// - `Y` /// - `YES` /// -/// # Errors -/// Returns an error if an unsigned `u64` or a float `f64` value is not -/// a *zero* or a *one*. -/// /// # Returns /// A [`Some`] with the boolean (`bool`) value of a string, /// boolean, or number. @@ -105,6 +94,7 @@ where /// * an `i64` value. /// * a `null` value. /// * any *de-serialization* errors. +/// * ex. an unsigned `u64` or a float `f64` value is not a *zero* or a *one*. /// pub fn as_opt_bool<'de, D>(deserializer: D) -> Result, D::Error> where From 897efc240908c474d687b1a0e802b9a5884c46a8 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Sat, 18 Mar 2023 02:22:36 -0400 Subject: [PATCH 09/22] change examples path --- Cargo.toml | 20 +++++++++++++++++++ .../{as_opt_bool.rs => optionals/as_bool.rs} | 0 .../{as_opt_f64.rs => optionals/as_f64.rs} | 0 .../{as_opt_i64.rs => optionals/as_i64.rs} | 0 .../as_string.rs} | 0 .../{as_opt_u64.rs => optionals/as_u64.rs} | 0 6 files changed, 20 insertions(+) rename examples/{as_opt_bool.rs => optionals/as_bool.rs} (100%) rename examples/{as_opt_f64.rs => optionals/as_f64.rs} (100%) rename examples/{as_opt_i64.rs => optionals/as_i64.rs} (100%) rename examples/{as_opt_string.rs => optionals/as_string.rs} (100%) rename examples/{as_opt_u64.rs => optionals/as_u64.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index c921aa6..d5fa9d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,3 +82,23 @@ path = "benches/as_bool.rs" name = "serde_with" harness = false path = "benches/serde_with.rs" + +[[example]] +name = "opt_as_bool" +path = "examples/optionals/as_bool.rs" + +[[example]] +name = "opt_as_f64" +path = "examples/optionals/as_f64.rs" + +[[example]] +name = "opt_as_i64" +path = "examples/optionals/as_i64.rs" + +[[example]] +name = "opt_as_string" +path = "examples/optionals/as_string.rs" + +[[example]] +name = "opt_as_u64" +path = "examples/optionals/as_u64.rs" diff --git a/examples/as_opt_bool.rs b/examples/optionals/as_bool.rs similarity index 100% rename from examples/as_opt_bool.rs rename to examples/optionals/as_bool.rs diff --git a/examples/as_opt_f64.rs b/examples/optionals/as_f64.rs similarity index 100% rename from examples/as_opt_f64.rs rename to examples/optionals/as_f64.rs diff --git a/examples/as_opt_i64.rs b/examples/optionals/as_i64.rs similarity index 100% rename from examples/as_opt_i64.rs rename to examples/optionals/as_i64.rs diff --git a/examples/as_opt_string.rs b/examples/optionals/as_string.rs similarity index 100% rename from examples/as_opt_string.rs rename to examples/optionals/as_string.rs diff --git a/examples/as_opt_u64.rs b/examples/optionals/as_u64.rs similarity index 100% rename from examples/as_opt_u64.rs rename to examples/optionals/as_u64.rs From 525a6d70d0489d6b7293144a5942aa0476409530 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Sat, 18 Mar 2023 02:26:30 -0400 Subject: [PATCH 10/22] reformat docs --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5152e3b..629a283 100644 --- a/README.md +++ b/README.md @@ -67,16 +67,11 @@ fn main() -> Result<(), Box> { ## Exported Functions -- [`as_bool`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_bool.html) -- [`as_f64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_f64.html) -- [`as_i64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_i64.html) -- [`as_string`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_string.html) -- [`as_u64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_u64.html) -- [`as_opt_bool`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_bool.html) -- [`as_opt_f64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_f64.html) -- [`as_opt_i64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_i64.html) -- [`as_opt_string`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_string.html) -- [`as_opt_u64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_u64.html) +- [`as_bool`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_bool.html) / [`as_opt_bool`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_bool.html) +- [`as_f64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_f64.html) / [`as_opt_f64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_f64.html) +- [`as_i64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_i64.html) / [`as_opt_i64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_i64.html) +- [`as_string`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_string.html) / [`as_opt_string`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_string.html) +- [`as_u64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_u64.html) / [`as_opt_u64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_u64.html) ## Examples From b0ec0e26dca03ea216756397e463d8bb2564289d Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Mon, 20 Mar 2023 13:30:16 -0400 Subject: [PATCH 11/22] `as_opt_string` should deserialize empty strings `to Some(String::new())` rather than `None` --- examples/demo.rs | 2 +- examples/optionals/as_string.rs | 2 +- src/de_impl_opt.rs | 6 +----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/examples/demo.rs b/examples/demo.rs index 0edd90c..29209fd 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -42,7 +42,7 @@ fn main() -> Result<(), Box> { assert!(s.is_active); assert_eq!(s.num_attempts, 0); assert_eq!(s.grade, 81.0); - assert_eq!(s.notes, None); + assert_eq!(s.notes, Some("".into())); assert_eq!(s.confidence, None); let string = r#" diff --git a/examples/optionals/as_string.rs b/examples/optionals/as_string.rs index a9e8c0b..36b9ece 100644 --- a/examples/optionals/as_string.rs +++ b/examples/optionals/as_string.rs @@ -26,7 +26,7 @@ fn main() -> Result<()> { }"#; let m: Msg = serde_json::from_str(data).unwrap(); - assert_eq!(m.timestamp, None); + assert_eq!(m.timestamp, Some("".into())); trace!(" {m:?}"); trace!("With Null: "); diff --git a/src/de_impl_opt.rs b/src/de_impl_opt.rs index e3c2c6a..6173a7d 100644 --- a/src/de_impl_opt.rs +++ b/src/de_impl_opt.rs @@ -424,11 +424,7 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalStringWithVisitor { where E: de::Error, { - if v.is_empty() { - Ok(None) - } else { - Ok(Some(v.to_owned())) - } + Ok(Some(v.to_owned())) } /// We encounter a `null` value; this default implementation returns an From 78e8a051f92f1e757f572cde1161cd55e37fc2b0 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Mon, 20 Mar 2023 13:34:59 -0400 Subject: [PATCH 12/22] as_bool: rename `timestamp` to `archived` in examples to resolve ambiguity --- examples/as_bool.rs | 38 ++++++++++++++--------------- examples/optionals/as_bool.rs | 46 +++++++++++++++++------------------ 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/examples/as_bool.rs b/examples/as_bool.rs index c2888c6..2014eac 100644 --- a/examples/as_bool.rs +++ b/examples/as_bool.rs @@ -10,7 +10,7 @@ use serde_this_or_that::as_bool; #[derive(Clone, Debug, Deserialize)] pub struct Msg { #[serde(deserialize_with = "as_bool")] - pub timestamp: bool, + pub archived: bool, } // A simple type alias so as to DRY. @@ -22,99 +22,99 @@ fn main() -> Result<()> { trace!("With Empty String:"); let data = r#" { - "timestamp": "" + "archived": "" }"#; let m: Msg = serde_json::from_str(data).unwrap(); - assert!(!m.timestamp); + assert!(!m.archived); trace!(" {m:?}"); trace!("With Null: "); let data = r#" { - "timestamp": null + "archived": null }"#; let m: Msg = serde_json::from_str(data).unwrap(); - assert!(!m.timestamp); + assert!(!m.archived); trace!(" {m:?}"); trace!("With Zero (0):"); let data = r#" { - "timestamp": 0 + "archived": 0 }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert!(!m.timestamp); + assert!(!m.archived); trace!("With One (1):"); let data = r#" { - "timestamp": 1 + "archived": 1 }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert!(m.timestamp); + assert!(m.archived); trace!("With String (truthy #1):"); let data = r#" { - "timestamp": "tRuE" + "archived": "tRuE" }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert!(m.timestamp); + assert!(m.archived); trace!("With String (truthy #2):"); let data = r#" { - "timestamp": "Y" + "archived": "Y" }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert!(m.timestamp); + assert!(m.archived); trace!("With String (falsy):"); let data = r#" { - "timestamp": "nope!" + "archived": "nope!" }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert!(!m.timestamp); + assert!(!m.archived); trace!("With String (Invalid Numeric):"); let data = r#" { - "timestamp": "123456789076543210" + "archived": "123456789076543210" }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert!(!m.timestamp); + assert!(!m.archived); trace!("With U64:"); let data = r#" { - "timestamp": 123456789076543210 + "archived": 123456789076543210 }"#; if let Err(e) = serde_json::from_str::(data) { @@ -128,7 +128,7 @@ fn main() -> Result<()> { let data = r#" { - "timestamp": -123 + "archived": -123 }"#; if let Err(e) = serde_json::from_str::(data) { diff --git a/examples/optionals/as_bool.rs b/examples/optionals/as_bool.rs index 6ab9192..4e22570 100644 --- a/examples/optionals/as_bool.rs +++ b/examples/optionals/as_bool.rs @@ -10,7 +10,7 @@ use serde_this_or_that::as_opt_bool; #[derive(Clone, Debug, Deserialize)] pub struct Msg { #[serde(deserialize_with = "as_opt_bool")] - pub timestamp: Option, + pub archived: Option, } // A simple type alias so as to DRY. @@ -22,112 +22,112 @@ fn main() -> Result<()> { trace!("With Empty String:"); let data = r#" { - "timestamp": "" + "archived": "" }"#; let m: Msg = serde_json::from_str(data).unwrap(); - assert_eq!(m.timestamp, Some(false)); + assert_eq!(m.archived, Some(false)); trace!(" {m:?}"); trace!("With I64:"); let data = r#" { - "timestamp": -123 + "archived": -123 }"#; let m: Msg = serde_json::from_str(data).unwrap(); - assert_eq!(m.timestamp, None); + assert_eq!(m.archived, None); trace!(" {m:?}"); trace!("With Null: "); let data = r#" { - "timestamp": null + "archived": null }"#; let m: Msg = serde_json::from_str(data).unwrap(); - assert_eq!(m.timestamp, None); + assert_eq!(m.archived, None); trace!(" {m:?}"); trace!("With Zero (0):"); let data = r#" { - "timestamp": 0 + "archived": 0 }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert_eq!(m.timestamp, Some(false)); + assert_eq!(m.archived, Some(false)); trace!("With One (1):"); let data = r#" { - "timestamp": 1 + "archived": 1 }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert_eq!(m.timestamp, Some(true)); + assert_eq!(m.archived, Some(true)); trace!("With String (truthy #1):"); let data = r#" { - "timestamp": "tRuE" + "archived": "tRuE" }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert_eq!(m.timestamp, Some(true)); + assert_eq!(m.archived, Some(true)); trace!("With String (truthy #2):"); let data = r#" { - "timestamp": "Y" + "archived": "Y" }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert_eq!(m.timestamp, Some(true)); + assert_eq!(m.archived, Some(true)); trace!("With String (falsy):"); let data = r#" { - "timestamp": "nope!" + "archived": "nope!" }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert_eq!(m.timestamp, Some(false)); + assert_eq!(m.archived, Some(false)); trace!("With String (Invalid Numeric):"); let data = r#" { - "timestamp": "123456789076543210" + "archived": "123456789076543210" }"#; let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert_eq!(m.timestamp, Some(false)); + assert_eq!(m.archived, Some(false)); trace!("With U64:"); let data = r#" { - "timestamp": 123456789076543210 + "archived": 123456789076543210 }"#; - if matches!(serde_json::from_str::(data), Ok(m) if m.timestamp.is_none()) { + if matches!(serde_json::from_str::(data), Ok(m) if m.archived.is_none()) { trace!(" None"); } else { error!(" ERROR! no error should have occurred."); @@ -138,10 +138,10 @@ fn main() -> Result<()> { let data = r#" { - "timestamp": -123 + "archived": -123 }"#; - if matches!(serde_json::from_str::(data), Ok(m) if m.timestamp.is_none()) { + if matches!(serde_json::from_str::(data), Ok(m) if m.archived.is_none()) { trace!(" None"); } else { error!(" ERROR! no error should have occurred."); From 4b293168dd80ace8100b0740c0a9160c2f6d6d5e Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Mon, 20 Mar 2023 13:45:01 -0400 Subject: [PATCH 13/22] Update src/de_impl_opt.rs Co-authored-by: Cornelius Roemer --- src/de_impl_opt.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/de_impl_opt.rs b/src/de_impl_opt.rs index 6173a7d..b87e821 100644 --- a/src/de_impl_opt.rs +++ b/src/de_impl_opt.rs @@ -38,6 +38,7 @@ where /// * any *de-serialization* errors. /// * ex. a string is non-empty and not a valid numeric value. /// * ex. the signed value `i64` represents a *negative* number. +/// * ex. float `f64` represents a *negative* number `< -0.5`, or `NaN`. /// pub fn as_opt_u64<'de, D>(deserializer: D) -> Result, D::Error> where From daedf251b7989464069a56da907c2cb0dd580d96 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Mon, 20 Mar 2023 14:05:18 -0400 Subject: [PATCH 14/22] `as_opt_bool`: deserialize only "truthy" or "falsy" values to `Some(bool)`, and others to `None` --- examples/optionals/as_bool.rs | 18 +++++++++++++++--- src/de_impl_opt.rs | 26 +++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/examples/optionals/as_bool.rs b/examples/optionals/as_bool.rs index 4e22570..d0a9466 100644 --- a/examples/optionals/as_bool.rs +++ b/examples/optionals/as_bool.rs @@ -26,7 +26,7 @@ fn main() -> Result<()> { }"#; let m: Msg = serde_json::from_str(data).unwrap(); - assert_eq!(m.archived, Some(false)); + assert_eq!(m.archived, None); trace!(" {m:?}"); trace!("With I64:"); @@ -100,7 +100,7 @@ fn main() -> Result<()> { let data = r#" { - "archived": "nope!" + "archived": "ng" }"#; let m: Msg = serde_json::from_str(data).unwrap(); @@ -108,6 +108,18 @@ fn main() -> Result<()> { trace!(" {m:?}"); assert_eq!(m.archived, Some(false)); + trace!("With String (Invalid):"); + + let data = r#" + { + "archived": "nope!" + }"#; + + let m: Msg = serde_json::from_str(data).unwrap(); + + trace!(" {m:?}"); + assert_eq!(m.archived, None); + trace!("With String (Invalid Numeric):"); let data = r#" @@ -118,7 +130,7 @@ fn main() -> Result<()> { let m: Msg = serde_json::from_str(data).unwrap(); trace!(" {m:?}"); - assert_eq!(m.archived, Some(false)); + assert_eq!(m.archived, None); trace!("With U64:"); diff --git a/src/de_impl_opt.rs b/src/de_impl_opt.rs index b87e821..3569317 100644 --- a/src/de_impl_opt.rs +++ b/src/de_impl_opt.rs @@ -87,11 +87,28 @@ where /// - `Y` /// - `YES` /// +/// # Falsy String Values +/// > Note: the pattern matching is *case insensitive*, so `NO` or `no` +/// > works just the same. +/// +/// These are the following "falsy" string values that result in a +/// boolean value of `false`: +/// +/// - `0` +/// - `NG` ([antonym for `OK`](https://english.stackexchange.com/a/586568/461000)) +/// - `OFF` +/// - `F` +/// - `FALSE` +/// - `N` +/// - `NO` +/// /// # Returns /// A [`Some`] with the boolean (`bool`) value of a string, /// boolean, or number. /// /// A [`None`] in the case of: +/// * a `str` value which does not match any of the ["truthy"](#truthy-string-values) +/// or ["falsy"](#falsy-string-values) values as defined above. /// * an `i64` value. /// * a `null` value. /// * any *de-serialization* errors. @@ -363,12 +380,15 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalBoolWithVisitor { "f" | "F" | "false" | "False" | "0" => Ok(Some(false)), other => { // So from the above, we've already matched the following - // "truthy" phrases: ["T", "1"]. + // "truthy" phrases: ["T", "1"] + // and the following "falsy" phrases: ["F", "0"]. // To be completely thorough, we also need to do a case- - // insensitive match on ["OK", "ON", "TRUE", "Y", "YES"]. + // insensitive match on ["OK", "ON", "TRUE", "Y", "YES"] + // and its counterpart, ["NG", "OFF", "FALSE", "N", "NO"]. match other.to_uppercase().as_str() { "OK" | "ON" | "TRUE" | "Y" | "YES" => Ok(Some(true)), - _ => Ok(Some(false)), + "NG" | "OFF" | "FALSE" | "N" | "NO" => Ok(Some(false)), + _ => Ok(None), } } } From fec9271737344047c7e4790a3fbeecbce42b6d99 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Mon, 20 Mar 2023 14:18:47 -0400 Subject: [PATCH 15/22] fix: resolve conflict in docs to agree with updated logic --- src/de_impl_opt.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/de_impl_opt.rs b/src/de_impl_opt.rs index 3569317..48975d0 100644 --- a/src/de_impl_opt.rs +++ b/src/de_impl_opt.rs @@ -123,7 +123,7 @@ where /// De-serialize either a `str`, `bool`, `i64`, `f64`, or `u64` /// as an (owned) *string* value wrapped in [`Some`], -/// and an empty string or `null` value as [`None`]. +/// and a `null` value as [`None`]. /// /// # Returns /// A [`Some`] with the owned `String` value of a string, @@ -131,7 +131,6 @@ where /// /// A [`None`] in the case of: /// * a `null` value. -/// * an empty string. /// * any *de-serialization* errors. /// pub fn as_opt_string<'de, D>(deserializer: D) -> Result, D::Error> From 0951874a6085a654d3422a5508f2d49ce3cad967 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Fri, 24 Jan 2025 23:08:48 -0500 Subject: [PATCH 16/22] add tests --- src/de_impl.rs | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/de_impl.rs b/src/de_impl.rs index 9ac7d30..ddc5051 100644 --- a/src/de_impl.rs +++ b/src/de_impl.rs @@ -412,3 +412,94 @@ impl<'de> de::Visitor<'de> for DeserializeStringWithVisitor { Ok(String::new()) } } + +#[cfg(test)] +mod tests { + use super::*; + mod as_string_tests { + use super::*; + use serde::Deserialize; + + #[derive(Debug, PartialEq, Deserialize)] + struct TestStrStruct { + #[serde(deserialize_with = "as_string")] + field: String, + } + + #[test] + fn test_as_str_with_string() { + let json = r#"{"field": "Hello"}"#; + let deserialized: TestStrStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStrStruct { field: "Hello".to_owned() }); + } + + #[test] + fn test_as_str_with_null() { + let json = r#"{"field": null}"#; + let deserialized: TestStrStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStrStruct { field: "".to_owned() }); + } + + #[test] + fn test_as_str_with_number() { + let json = r#"{"field": 123}"#; + let deserialized: TestStrStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStrStruct { field: "123".to_owned() }); + } + } + + mod as_bool_tests { + use super::*; + use serde::Deserialize; + + #[derive(Debug, PartialEq, Deserialize)] + struct TestStruct { + #[serde(deserialize_with = "as_bool")] + field: bool, + } + + #[test] + fn test_ok_values() { + let json = r#"{"field": "OK"}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: true }); + + let json = r#"{"field": true}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: true }); + + let json = r#"{"field": "Y"}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: true }); + } + + #[test] + fn test_ng_values() { + let json = r#"{"field": "NG"}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: false }); + + let json = r#"{"field": false}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: false }); + + let json = r#"{"field": "NO"}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: false }); + } + + #[test] + fn test_null_value() { + let json = r#"{"field": null}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: false }); + } + + #[test] + fn test_invalid_values() { + let json = r#"{"field": "INVALID"}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: false }); + } + } +} \ No newline at end of file From d114464878c87265c48a9fba87e50fa3da7207f6 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Fri, 24 Jan 2025 23:19:23 -0500 Subject: [PATCH 17/22] add moar tests --- src/de_impl.rs | 140 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 4 deletions(-) diff --git a/src/de_impl.rs b/src/de_impl.rs index ddc5051..b89df8d 100644 --- a/src/de_impl.rs +++ b/src/de_impl.rs @@ -430,21 +430,36 @@ mod tests { fn test_as_str_with_string() { let json = r#"{"field": "Hello"}"#; let deserialized: TestStrStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStrStruct { field: "Hello".to_owned() }); + assert_eq!( + deserialized, + TestStrStruct { + field: "Hello".to_owned() + } + ); } #[test] fn test_as_str_with_null() { let json = r#"{"field": null}"#; let deserialized: TestStrStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStrStruct { field: "".to_owned() }); + assert_eq!( + deserialized, + TestStrStruct { + field: "".to_owned() + } + ); } #[test] fn test_as_str_with_number() { let json = r#"{"field": 123}"#; let deserialized: TestStrStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStrStruct { field: "123".to_owned() }); + assert_eq!( + deserialized, + TestStrStruct { + field: "123".to_owned() + } + ); } } @@ -502,4 +517,121 @@ mod tests { assert_eq!(deserialized, TestStruct { field: false }); } } -} \ No newline at end of file + + mod as_f64_tests { + use super::*; + use serde::Deserialize; + + #[derive(Debug, PartialEq, Deserialize)] + struct TestStruct { + #[serde(deserialize_with = "as_f64")] + field: f64, + } + + #[test] + fn test_as_f64_with_number() { + let json = r#"{"field": 123.45}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 123.45 }); + } + + #[test] + fn test_as_f64_with_integer() { + let json = r#"{"field": 123}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 123.0 }); + } + + #[test] + fn test_as_f64_with_string() { + let json = r#"{"field": "123.45"}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 123.45 }); + } + + #[test] + fn test_as_f64_with_null() { + let json = r#"{"field": null}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 0.0 }); + } + } + + mod as_i64_tests { + use super::*; + use serde::Deserialize; + + #[derive(Debug, PartialEq, Deserialize)] + struct TestStruct { + #[serde(deserialize_with = "as_i64")] + field: i64, + } + + #[test] + fn test_as_i64_with_integer() { + let json = r#"{"field": 123}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 123 }); + } + + #[test] + fn test_as_i64_with_string() { + let json = r#"{"field": "123"}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 123 }); + } + + #[test] + fn test_as_i64_with_float() { + let json = r#"{"field": 123.45}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 123 }); + } + + #[test] + fn test_as_i64_with_null() { + let json = r#"{"field": null}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 0 }); + } + } + + mod as_u64_tests { + use super::*; + use serde::Deserialize; + + #[derive(Debug, PartialEq, Deserialize)] + struct TestStruct { + #[serde(deserialize_with = "as_u64")] + field: u64, + } + + #[test] + fn test_as_u64_with_integer() { + let json = r#"{"field": 123}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 123 }); + } + + #[test] + fn test_as_u64_with_string() { + let json = r#"{"field": "123"}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 123 }); + } + + #[test] + fn test_as_u64_with_float() { + let json = r#"{"field": 123.45}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 123 }); + } + + #[test] + fn test_as_u64_with_null() { + let json = r#"{"field": null}"#; + let deserialized: TestStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestStruct { field: 0 }); + } + } +} From 385db28b34a66b52bbe537be8f323516a02c19f5 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Fri, 24 Jan 2025 23:31:49 -0500 Subject: [PATCH 18/22] add tests --- src/de_impl.rs | 192 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 126 insertions(+), 66 deletions(-) diff --git a/src/de_impl.rs b/src/de_impl.rs index b89df8d..eab52cc 100644 --- a/src/de_impl.rs +++ b/src/de_impl.rs @@ -416,9 +416,10 @@ impl<'de> de::Visitor<'de> for DeserializeStringWithVisitor { #[cfg(test)] mod tests { use super::*; + use serde::Deserialize; + mod as_string_tests { use super::*; - use serde::Deserialize; #[derive(Debug, PartialEq, Deserialize)] struct TestStrStruct { @@ -427,7 +428,7 @@ mod tests { } #[test] - fn test_as_str_with_string() { + fn test_as_string_with_string() { let json = r#"{"field": "Hello"}"#; let deserialized: TestStrStruct = serde_json::from_str(json).unwrap(); assert_eq!( @@ -439,7 +440,7 @@ mod tests { } #[test] - fn test_as_str_with_null() { + fn test_as_string_with_null() { let json = r#"{"field": null}"#; let deserialized: TestStrStruct = serde_json::from_str(json).unwrap(); assert_eq!( @@ -451,7 +452,7 @@ mod tests { } #[test] - fn test_as_str_with_number() { + fn test_as_string_with_number() { let json = r#"{"field": 123}"#; let deserialized: TestStrStruct = serde_json::from_str(json).unwrap(); assert_eq!( @@ -461,108 +462,140 @@ mod tests { } ); } + + #[test] + fn test_as_string_with_boolean() { + let json = r#"{"field": true}"#; + let deserialized: TestStrStruct = serde_json::from_str(json).unwrap(); + assert_eq!( + deserialized, + TestStrStruct { + field: "true".to_owned() + } + ); + } + + #[test] + fn test_as_string_with_empty_string() { + let json = r#"{"field": ""}"#; + let deserialized: TestStrStruct = serde_json::from_str(json).unwrap(); + assert_eq!( + deserialized, + TestStrStruct { + field: "".to_owned() + } + ); + } } mod as_bool_tests { use super::*; - use serde::Deserialize; #[derive(Debug, PartialEq, Deserialize)] - struct TestStruct { + struct TestBoolStruct { #[serde(deserialize_with = "as_bool")] field: bool, } #[test] - fn test_ok_values() { - let json = r#"{"field": "OK"}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: true }); - - let json = r#"{"field": true}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: true }); - - let json = r#"{"field": "Y"}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: true }); + fn test_truthy_values() { + let truthy_values = ["1", "OK", "ON", "T", "TRUE", "Y", "YES"]; + for value in truthy_values { + let json = format!(r#"{{"field": "{}"}}"#, value); + let deserialized: TestBoolStruct = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, TestBoolStruct { field: true }); + } } #[test] - fn test_ng_values() { - let json = r#"{"field": "NG"}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: false }); - - let json = r#"{"field": false}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: false }); - - let json = r#"{"field": "NO"}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: false }); + fn test_falsy_values() { + let falsy_values = ["0", "OFF", "F", "FALSE", "N", "NO"]; + for value in falsy_values { + let json = format!(r#"{{"field": "{}"}}"#, value); + let deserialized: TestBoolStruct = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, TestBoolStruct { field: false }); + } } #[test] - fn test_null_value() { + fn test_as_bool_with_null() { let json = r#"{"field": null}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: false }); + let deserialized: TestBoolStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestBoolStruct { field: false }); } #[test] - fn test_invalid_values() { + fn test_invalid_boolean() { let json = r#"{"field": "INVALID"}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: false }); + let deserialized: TestBoolStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestBoolStruct { field: false }); } } mod as_f64_tests { use super::*; - use serde::Deserialize; #[derive(Debug, PartialEq, Deserialize)] - struct TestStruct { + struct TestF64Struct { #[serde(deserialize_with = "as_f64")] field: f64, } + #[test] + fn test_as_f64_with_large_number() { + let json = r#"{"field": 1e308}"#; + let deserialized: TestF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestF64Struct { field: 1e308 }); + } + + #[test] + fn test_as_f64_with_negative_number() { + let json = r#"{"field": -123.45}"#; + let deserialized: TestF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestF64Struct { field: -123.45 }); + } + #[test] fn test_as_f64_with_number() { let json = r#"{"field": 123.45}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 123.45 }); + let deserialized: TestF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestF64Struct { field: 123.45 }); } #[test] fn test_as_f64_with_integer() { let json = r#"{"field": 123}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 123.0 }); + let deserialized: TestF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestF64Struct { field: 123.0 }); } #[test] fn test_as_f64_with_string() { let json = r#"{"field": "123.45"}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 123.45 }); + let deserialized: TestF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestF64Struct { field: 123.45 }); + } + + #[test] + fn test_as_f64_with_empty_string() { + let json = r#"{"field": ""}"#; + let deserialized = serde_json::from_str::(json).unwrap(); + assert_eq!(deserialized, TestF64Struct { field: 0.0 }); } #[test] fn test_as_f64_with_null() { let json = r#"{"field": null}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 0.0 }); + let deserialized: TestF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestF64Struct { field: 0.0 }); } } mod as_i64_tests { use super::*; - use serde::Deserialize; #[derive(Debug, PartialEq, Deserialize)] - struct TestStruct { + struct TestI64Struct { #[serde(deserialize_with = "as_i64")] field: i64, } @@ -570,38 +603,51 @@ mod tests { #[test] fn test_as_i64_with_integer() { let json = r#"{"field": 123}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 123 }); + let deserialized: TestI64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestI64Struct { field: 123 }); } #[test] fn test_as_i64_with_string() { let json = r#"{"field": "123"}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 123 }); + let deserialized: TestI64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestI64Struct { field: 123 }); } #[test] fn test_as_i64_with_float() { let json = r#"{"field": 123.45}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 123 }); + let deserialized: TestI64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestI64Struct { field: 123 }); } #[test] fn test_as_i64_with_null() { let json = r#"{"field": null}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 0 }); + let deserialized: TestI64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestI64Struct { field: 0 }); + } + + #[test] + fn test_as_i64_with_max_value() { + let json = format!(r#"{{"field": {}}}"#, i64::MAX); + let deserialized: TestI64Struct = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, TestI64Struct { field: i64::MAX }); + } + + #[test] + fn test_as_i64_with_overflow() { + let json = format!(r#"{{"field": {}}}"#, u64::MAX); + let deserialized = serde_json::from_str::(&json); + assert!(deserialized.is_err()); } } mod as_u64_tests { use super::*; - use serde::Deserialize; #[derive(Debug, PartialEq, Deserialize)] - struct TestStruct { + struct TestU64Struct { #[serde(deserialize_with = "as_u64")] field: u64, } @@ -609,29 +655,43 @@ mod tests { #[test] fn test_as_u64_with_integer() { let json = r#"{"field": 123}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 123 }); + let deserialized: TestU64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestU64Struct { field: 123 }); } #[test] fn test_as_u64_with_string() { let json = r#"{"field": "123"}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 123 }); + let deserialized: TestU64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestU64Struct { field: 123 }); } #[test] fn test_as_u64_with_float() { let json = r#"{"field": 123.45}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 123 }); + let deserialized: TestU64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestU64Struct { field: 123 }); } #[test] fn test_as_u64_with_null() { let json = r#"{"field": null}"#; - let deserialized: TestStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestStruct { field: 0 }); + let deserialized: TestU64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestU64Struct { field: 0 }); + } + + #[test] + fn test_as_u64_with_negative_number() { + let json = r#"{"field": -1}"#; + let deserialized = serde_json::from_str::(json); + assert!(deserialized.is_err()); + } + + #[test] + fn test_as_u64_with_large_value() { + let json = format!(r#"{{"field": {}}}"#, u64::MAX); + let deserialized: TestU64Struct = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, TestU64Struct { field: u64::MAX }); } } } From b6a1bc534b72ceb4537726636ce8bc211e76b66a Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Fri, 24 Jan 2025 23:37:27 -0500 Subject: [PATCH 19/22] add tests for de_impl_opt.rs --- src/de_impl_opt.rs | 143 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/src/de_impl_opt.rs b/src/de_impl_opt.rs index 48975d0..a339f19 100644 --- a/src/de_impl_opt.rs +++ b/src/de_impl_opt.rs @@ -456,3 +456,146 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalStringWithVisitor { Ok(None) } } + +#[cfg(test)] +mod tests { + use super::*; + use serde::Deserialize; + + mod as_opt_bool_tests { + use super::*; + + #[derive(Debug, PartialEq, Deserialize)] + struct TestOptBoolStruct { + #[serde(deserialize_with = "as_opt_bool")] + field: Option, + } + + #[test] + fn test_as_opt_bool_with_bool() { + let json = r#"{"field": true}"#; + let deserialized: TestOptBoolStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptBoolStruct { field: Some(true) }); + } + + #[test] + fn test_as_opt_bool_with_string() { + let json = r#"{"field": "true"}"#; + let deserialized: TestOptBoolStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptBoolStruct { field: Some(true) }); + } + + #[test] + fn test_as_opt_bool_with_null() { + let json = r#"{"field": null}"#; + let deserialized: TestOptBoolStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptBoolStruct { field: None }); + } + } + + mod as_opt_f64_tests { + use super::*; + + #[derive(Debug, PartialEq, Deserialize)] + struct TestOptF64Struct { + #[serde(deserialize_with = "as_opt_f64")] + field: Option, + } + + #[test] + fn test_as_opt_f64_with_float() { + let json = r#"{"field": 123.45}"#; + let deserialized: TestOptF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!( + deserialized, + TestOptF64Struct { + field: Some(123.45) + } + ); + } + + #[test] + fn test_as_opt_f64_with_null() { + let json = r#"{"field": null}"#; + let deserialized: TestOptF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptF64Struct { field: None }); + } + } + + mod as_opt_i64_tests { + use super::*; + + #[derive(Debug, PartialEq, Deserialize)] + struct TestOptI64Struct { + #[serde(deserialize_with = "as_opt_i64")] + field: Option, + } + + #[test] + fn test_as_opt_i64_with_integer() { + let json = r#"{"field": 123}"#; + let deserialized: TestOptI64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptI64Struct { field: Some(123) }); + } + + #[test] + fn test_as_opt_i64_with_null() { + let json = r#"{"field": null}"#; + let deserialized: TestOptI64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptI64Struct { field: None }); + } + } + + mod as_opt_string_tests { + use super::*; + + #[derive(Debug, PartialEq, Deserialize)] + struct TestOptStringStruct { + #[serde(deserialize_with = "as_opt_string")] + field: Option, + } + + #[test] + fn test_as_opt_string_with_string() { + let json = r#"{"field": "Hello"}"#; + let deserialized: TestOptStringStruct = serde_json::from_str(json).unwrap(); + assert_eq!( + deserialized, + TestOptStringStruct { + field: Some("Hello".to_owned()) + } + ); + } + + #[test] + fn test_as_opt_string_with_null() { + let json = r#"{"field": null}"#; + let deserialized: TestOptStringStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptStringStruct { field: None }); + } + } + + mod as_opt_u64_tests { + use super::*; + + #[derive(Debug, PartialEq, Deserialize)] + struct TestOptU64Struct { + #[serde(deserialize_with = "as_opt_u64")] + field: Option, + } + + #[test] + fn test_as_opt_u64_with_integer() { + let json = r#"{"field": 123}"#; + let deserialized: TestOptU64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptU64Struct { field: Some(123) }); + } + + #[test] + fn test_as_opt_u64_with_null() { + let json = r#"{"field": null}"#; + let deserialized: TestOptU64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptU64Struct { field: None }); + } + } +} From be4309d45a780b5c44f5203a3e1d5875b6e9e969 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Fri, 24 Jan 2025 23:42:51 -0500 Subject: [PATCH 20/22] add tests for de_impl_opt.rs --- src/de_impl_opt.rs | 135 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 9 deletions(-) diff --git a/src/de_impl_opt.rs b/src/de_impl_opt.rs index a339f19..a310b02 100644 --- a/src/de_impl_opt.rs +++ b/src/de_impl_opt.rs @@ -462,6 +462,7 @@ mod tests { use super::*; use serde::Deserialize; + // Tests for as_opt_bool mod as_opt_bool_tests { use super::*; @@ -472,17 +473,23 @@ mod tests { } #[test] - fn test_as_opt_bool_with_bool() { - let json = r#"{"field": true}"#; - let deserialized: TestOptBoolStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestOptBoolStruct { field: Some(true) }); + fn test_as_opt_bool_with_truthy_values() { + let truthy_values = ["1", "OK", "ON", "T", "TRUE", "Y", "YES"]; + for value in truthy_values { + let json = format!(r#"{{"field": "{}"}}"#, value); + let deserialized: TestOptBoolStruct = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, TestOptBoolStruct { field: Some(true) }); + } } #[test] - fn test_as_opt_bool_with_string() { - let json = r#"{"field": "true"}"#; - let deserialized: TestOptBoolStruct = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized, TestOptBoolStruct { field: Some(true) }); + fn test_as_opt_bool_with_falsy_values() { + let falsy_values = ["0", "OFF", "F", "FALSE", "N", "NO"]; + for value in falsy_values { + let json = format!(r#"{{"field": "{}"}}"#, value); + let deserialized: TestOptBoolStruct = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, TestOptBoolStruct { field: Some(false) }); + } } #[test] @@ -491,8 +498,16 @@ mod tests { let deserialized: TestOptBoolStruct = serde_json::from_str(json).unwrap(); assert_eq!(deserialized, TestOptBoolStruct { field: None }); } + + #[test] + fn test_as_opt_bool_with_invalid() { + let json = r#"{"field": "INVALID"}"#; + let deserialized: TestOptBoolStruct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptBoolStruct { field: None }); + } } + // Tests for as_opt_f64 mod as_opt_f64_tests { use super::*; @@ -503,7 +518,26 @@ mod tests { } #[test] - fn test_as_opt_f64_with_float() { + fn test_as_opt_f64_with_large_number() { + let json = r#"{"field": 1e308}"#; + let deserialized: TestOptF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptF64Struct { field: Some(1e308) }); + } + + #[test] + fn test_as_opt_f64_with_negative_number() { + let json = r#"{"field": -123.45}"#; + let deserialized: TestOptF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!( + deserialized, + TestOptF64Struct { + field: Some(-123.45) + } + ); + } + + #[test] + fn test_as_opt_f64_with_number() { let json = r#"{"field": 123.45}"#; let deserialized: TestOptF64Struct = serde_json::from_str(json).unwrap(); assert_eq!( @@ -514,14 +548,29 @@ mod tests { ); } + #[test] + fn test_as_opt_f64_with_integer() { + let json = r#"{"field": 123}"#; + let deserialized: TestOptF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptF64Struct { field: Some(123.0) }); + } + #[test] fn test_as_opt_f64_with_null() { let json = r#"{"field": null}"#; let deserialized: TestOptF64Struct = serde_json::from_str(json).unwrap(); assert_eq!(deserialized, TestOptF64Struct { field: None }); } + + #[test] + fn test_as_opt_f64_with_invalid_string() { + let json = r#"{"field": "INVALID"}"#; + let deserialized: TestOptF64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptF64Struct { field: None }); + } } + // Tests for as_opt_i64 mod as_opt_i64_tests { use super::*; @@ -538,14 +587,36 @@ mod tests { assert_eq!(deserialized, TestOptI64Struct { field: Some(123) }); } + #[test] + fn test_as_opt_i64_with_string() { + let json = r#"{"field": "123"}"#; + let deserialized: TestOptI64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptI64Struct { field: Some(123) }); + } + + #[test] + fn test_as_opt_i64_with_float() { + let json = r#"{"field": 123.45}"#; + let deserialized: TestOptI64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptI64Struct { field: Some(123) }); + } + #[test] fn test_as_opt_i64_with_null() { let json = r#"{"field": null}"#; let deserialized: TestOptI64Struct = serde_json::from_str(json).unwrap(); assert_eq!(deserialized, TestOptI64Struct { field: None }); } + + #[test] + fn test_as_opt_i64_with_invalid() { + let json = r#"{"field": "INVALID"}"#; + let deserialized: TestOptI64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptI64Struct { field: None }); + } } + // Tests for as_opt_string mod as_opt_string_tests { use super::*; @@ -567,6 +638,30 @@ mod tests { ); } + #[test] + fn test_as_opt_string_with_number() { + let json = r#"{"field": 123}"#; + let deserialized: TestOptStringStruct = serde_json::from_str(json).unwrap(); + assert_eq!( + deserialized, + TestOptStringStruct { + field: Some("123".to_owned()) + } + ); + } + + #[test] + fn test_as_opt_string_with_boolean() { + let json = r#"{"field": true}"#; + let deserialized: TestOptStringStruct = serde_json::from_str(json).unwrap(); + assert_eq!( + deserialized, + TestOptStringStruct { + field: Some("true".to_owned()) + } + ); + } + #[test] fn test_as_opt_string_with_null() { let json = r#"{"field": null}"#; @@ -575,6 +670,7 @@ mod tests { } } + // Tests for as_opt_u64 mod as_opt_u64_tests { use super::*; @@ -591,11 +687,32 @@ mod tests { assert_eq!(deserialized, TestOptU64Struct { field: Some(123) }); } + #[test] + fn test_as_opt_u64_with_string() { + let json = r#"{"field": "123"}"#; + let deserialized: TestOptU64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptU64Struct { field: Some(123) }); + } + + #[test] + fn test_as_opt_u64_with_float() { + let json = r#"{"field": 123.45}"#; + let deserialized: TestOptU64Struct = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, TestOptU64Struct { field: Some(123) }); + } + #[test] fn test_as_opt_u64_with_null() { let json = r#"{"field": null}"#; let deserialized: TestOptU64Struct = serde_json::from_str(json).unwrap(); assert_eq!(deserialized, TestOptU64Struct { field: None }); } + + #[test] + fn test_as_opt_u64_with_negative() { + let json = r#"{"field": -1}"#; + let deserialized = serde_json::from_str::(json).unwrap(); + assert_eq!(deserialized, TestOptU64Struct { field: None }); + } } } From 580557086d1991a56412e854e48eea15ec765477 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Sat, 25 Jan 2025 00:03:29 -0500 Subject: [PATCH 21/22] Add release notes for `v0.5.0` --- CHANGELOG.md | 43 ++++++++++++++++++++++++++++++++++++++----- Cargo.toml | 26 +++++++++++++------------- README.md | 28 ++++++++++++++++++++-------- src/lib.rs | 2 +- 4 files changed, 72 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e13fe7..59730b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Possible header types: - `Breaking Changes` for any backwards-incompatible changes. ## [Unreleased] + +## v0.5.0 (2025-01-25) + +### Features + +- **Added support for optional deserialization**: + - New functions were introduced to handle `Option` types during deserialization: + - `as_opt_bool` – Returns `Option`. + - `as_opt_f64` – Returns `Option`. + - `as_opt_i64` – Returns `Option`. + - `as_opt_string` – Returns `Option`. + - `as_opt_u64` – Returns `Option`. + + These functions ensure that `null` values in the JSON input or deserialization errors are correctly deserialized into + `None`, + while valid values will + return the appropriate `Some(T)`. + +### Bug Fixes + +- Resolved an issue where the existing deserialization functions did not handle `Option` types, only direct types. + Now, + `null` values and errors are deserialized to `None`, making the handling of nulls more consistent with Serde's + standard + behavior. + +### Breaking Changes + +- No breaking changes introduced in this version. + ## v0.4.2 (2023-02-05) ### Bug Fixes @@ -32,24 +62,27 @@ Possible header types: ## v0.4.0 (2022-04-18) ### Features + - Add benchmarks to compare performance against `serde_with`. - Flatten some nested `match` arms into simpler `if` statements. - Update `as_bool` - - Update to check for a new "truthy" string value of `ON`. - - Add pattern matching to check common *true/false* values **before** converting the string - to uppercase, which should make it overall more efficient. + - Update to check for a new "truthy" string value of `ON`. + - Add pattern matching to check common *true/false* values **before** converting the string + to uppercase, which should make it overall more efficient. - `serde_this_or_that` is now on par - in terms of performance - with `serde_with`! This is truly great news. ## v0.3.0 (2022-04-17) ### Breaking Changes + - Remove dependency on the `derive` feature of `serde` - - Add it as an optional feature named `derive` instead. + - Add it as an optional feature named `derive` instead. ### Features + - Replace `utilities` keyword with `this-or-that`, as I want crate to be - searchable when someone types "this or that". + searchable when someone types "this or that". - Update docs. ## v0.2.0 (2022-04-17) diff --git a/Cargo.toml b/Cargo.toml index d5fa9d8..642da3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,23 @@ [package] name = "serde-this-or-that" -version = "0.4.2" -authors = ["Ritvik Nag "] +version = "0.5.0" +authors = ["Ritvik Nag "] description = "Custom deserialization for fields that can be specified as multiple types." documentation = "https://docs.rs/serde-this-or-that" repository = "https://github.com/rnag/serde-this-or-that" readme = "README.md" keywords = ["serde", - # I would have liked to add the below keyword, but of course - # crates.io has a limit of 5 keywords, so well.. that's that. :\ - # "utilities", - # And now surprisingly, I found this keyword to be super useful! - # I'm actually partially shocked that crates.io doesn't use the - # crate name to automagically satisfy user search requests. - "this-or-that", - # Of course, the rest that follow are also pretty solid too. - "deserialization", - "visitor", - "multiple-type"] + # I would have liked to add the below keyword, but of course + # crates.io has a limit of 5 keywords, so well.. that's that. :\ + # "utilities", + # And now surprisingly, I found this keyword to be super useful! + # I'm actually partially shocked that crates.io doesn't use the + # crate name to automagically satisfy user search requests. + "this-or-that", + # Of course, the rest that follow are also pretty solid too. + "deserialization", + "visitor", + "multiple-type"] categories = ["encoding"] license = "MIT" edition = "2021" diff --git a/README.md b/README.md index 629a283..0083634 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This crate works with Cargo with a `Cargo.toml` like: ```toml [dependencies] -serde-this-or-that = "0.4" +serde-this-or-that = "0.5" serde = { version = "1", features = ["derive"] } serde_json = "1" ``` @@ -67,15 +67,21 @@ fn main() -> Result<(), Box> { ## Exported Functions -- [`as_bool`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_bool.html) / [`as_opt_bool`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_bool.html) -- [`as_f64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_f64.html) / [`as_opt_f64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_f64.html) -- [`as_i64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_i64.html) / [`as_opt_i64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_i64.html) -- [`as_string`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_string.html) / [`as_opt_string`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_string.html) -- [`as_u64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_u64.html) / [`as_opt_u64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_u64.html) +- [`as_bool`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_bool.html) / [ + `as_opt_bool`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_bool.html) +- [`as_f64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_f64.html) / [ + `as_opt_f64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_f64.html) +- [`as_i64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_i64.html) / [ + `as_opt_i64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_i64.html) +- [`as_string`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_string.html) / [ + `as_opt_string`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_string.html) +- [`as_u64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_u64.html) / [ + `as_opt_u64`](https://docs.rs/serde-this-or-that/latest/serde_this_or_that/fn.as_opt_u64.html) ## Examples -You can check out sample usage of this crate in the [examples/](https://github.com/rnag/serde-this-or-that/tree/main/examples) +You can check out sample usage of this crate in +the [examples/](https://github.com/rnag/serde-this-or-that/tree/main/examples) folder in the project repo on GitHub. ## Performance @@ -94,9 +100,13 @@ The benchmarks live in the [benches/](https://github.com/rnag/serde-this-or-that folder, and can be run with `cargo bench`. [`Visitor`]: https://docs.serde.rs/serde/de/trait.Visitor.html + [untagged enum]: https://stackoverflow.com/a/66961340/10237506 + [serde_with]: https://docs.rs/serde_with + [`DisplayFromStr`]: https://docs.rs/serde_with/latest/serde_with/struct.DisplayFromStr.html + [`PickFirst`]: https://docs.rs/serde_with/latest/serde_with/struct.PickFirst.html ## Optionals @@ -104,7 +114,8 @@ folder, and can be run with `cargo bench`. The extra helper functions that begin with `as_opt`, return an `Option` of the respective data type `T`, rather than the type `T` itself (see [#4](https://github.com/rnag/serde-this-or-that/issues/4)). -On success, they return a value of `T` wrapped in [`Some`](https://doc.rust-lang.org/std/option/enum.Option.html#variant.Some). +On success, they return a value of `T` wrapped in [ +`Some`](https://doc.rust-lang.org/std/option/enum.Option.html#variant.Some). On error, or when there is a `null` value, or one of an *invalid* data type, the `as_opt` helper functions return [`None`](https://doc.rust-lang.org/std/option/enum.Option.html#variant.None) instead. @@ -117,6 +128,7 @@ to discuss a new feature or change. Check out the [Contributing][] section in the docs for more info. [Contributing]: CONTRIBUTING.md + [open an issue]: https://github.com/rnag/serde-this-or-that/issues ## License diff --git a/src/lib.rs b/src/lib.rs index 24e1321..6dc6c5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![doc(html_root_url = "https://docs.rs/serde-this-or-that/0.4.2")] +#![doc(html_root_url = "https://docs.rs/serde-this-or-that/0.5.0")] #![warn(rust_2018_idioms, missing_docs)] #![deny(warnings, dead_code, unused_imports, unused_mut)] From 4ea42a5d1904f6b701ea74d8f3ed681d09b8d154 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Sat, 25 Jan 2025 00:09:18 -0500 Subject: [PATCH 22/22] fix `clippy` errors --- src/de_impl.rs | 12 ++++++------ src/de_impl_opt.rs | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/de_impl.rs b/src/de_impl.rs index eab52cc..052b720 100644 --- a/src/de_impl.rs +++ b/src/de_impl.rs @@ -98,11 +98,11 @@ where deserializer.deserialize_any(DeserializeStringWithVisitor) } -/// TODO maybe update these definitions into a macro ..? +// TODO maybe update these definitions into a macro ..? struct DeserializeU64WithVisitor; -impl<'de> de::Visitor<'de> for DeserializeU64WithVisitor { +impl de::Visitor<'_> for DeserializeU64WithVisitor { type Value = u64; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -162,7 +162,7 @@ impl<'de> de::Visitor<'de> for DeserializeU64WithVisitor { struct DeserializeI64WithVisitor; -impl<'de> de::Visitor<'de> for DeserializeI64WithVisitor { +impl de::Visitor<'_> for DeserializeI64WithVisitor { type Value = i64; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -222,7 +222,7 @@ impl<'de> de::Visitor<'de> for DeserializeI64WithVisitor { struct DeserializeF64WithVisitor; -impl<'de> de::Visitor<'de> for DeserializeF64WithVisitor { +impl de::Visitor<'_> for DeserializeF64WithVisitor { type Value = f64; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -275,7 +275,7 @@ impl<'de> de::Visitor<'de> for DeserializeF64WithVisitor { struct DeserializeBoolWithVisitor; -impl<'de> de::Visitor<'de> for DeserializeBoolWithVisitor { +impl de::Visitor<'_> for DeserializeBoolWithVisitor { type Value = bool; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -361,7 +361,7 @@ impl<'de> de::Visitor<'de> for DeserializeBoolWithVisitor { struct DeserializeStringWithVisitor; -impl<'de> de::Visitor<'de> for DeserializeStringWithVisitor { +impl de::Visitor<'_> for DeserializeStringWithVisitor { type Value = String; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/src/de_impl_opt.rs b/src/de_impl_opt.rs index a310b02..bb53f15 100644 --- a/src/de_impl_opt.rs +++ b/src/de_impl_opt.rs @@ -140,11 +140,11 @@ where deserializer.deserialize_any(DeserializeOptionalStringWithVisitor) } -/// TODO maybe update these definitions into a macro ..? +// TODO maybe update these definitions into a macro ..? struct DeserializeOptionalU64WithVisitor; -impl<'de> de::Visitor<'de> for DeserializeOptionalU64WithVisitor { +impl de::Visitor<'_> for DeserializeOptionalU64WithVisitor { type Value = Option; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -206,7 +206,7 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalU64WithVisitor { struct DeserializeOptionalI64WithVisitor; -impl<'de> de::Visitor<'de> for DeserializeOptionalI64WithVisitor { +impl de::Visitor<'_> for DeserializeOptionalI64WithVisitor { type Value = Option; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -268,7 +268,7 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalI64WithVisitor { struct DeserializeOptionalF64WithVisitor; -impl<'de> de::Visitor<'de> for DeserializeOptionalF64WithVisitor { +impl de::Visitor<'_> for DeserializeOptionalF64WithVisitor { type Value = Option; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -322,7 +322,7 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalF64WithVisitor { struct DeserializeOptionalBoolWithVisitor; -impl<'de> de::Visitor<'de> for DeserializeOptionalBoolWithVisitor { +impl de::Visitor<'_> for DeserializeOptionalBoolWithVisitor { type Value = Option; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -405,7 +405,7 @@ impl<'de> de::Visitor<'de> for DeserializeOptionalBoolWithVisitor { struct DeserializeOptionalStringWithVisitor; -impl<'de> de::Visitor<'de> for DeserializeOptionalStringWithVisitor { +impl de::Visitor<'_> for DeserializeOptionalStringWithVisitor { type Value = Option; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {