From 34fbc974bd21bc1fe4fbc6aa9cb12bf3ca76369a Mon Sep 17 00:00:00 2001 From: Jonas Bushart Date: Thu, 26 May 2022 09:23:02 +0000 Subject: [PATCH 1/2] Add BoolFromInt adapter --- serde_with/src/de/impls.rs | 156 +++++++++++++++++++++++++++++++ serde_with/src/lib.rs | 49 ++++++++++ serde_with/src/ser/impls.rs | 9 ++ serde_with/tests/serde_as/lib.rs | 70 +++++++++++++- 4 files changed, 283 insertions(+), 1 deletion(-) diff --git a/serde_with/src/de/impls.rs b/serde_with/src/de/impls.rs index a6b8a177..fc960c3d 100644 --- a/serde_with/src/de/impls.rs +++ b/serde_with/src/de/impls.rs @@ -1349,4 +1349,160 @@ impl<'de> DeserializeAs<'de, Cow<'de, [u8]>> for BorrowCow { } } +impl<'de> DeserializeAs<'de, bool> for BoolFromInt { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct U8Visitor; + impl<'de> Visitor<'de> for U8Visitor { + type Value = bool; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("an integer 0 or 1") + } + + fn visit_u8(self, v: u8) -> Result + where + E: Error, + { + match v { + 0 => Ok(false), + 1 => Ok(true), + unexp => Err(Error::invalid_value( + Unexpected::Unsigned(unexp as u64), + &"0 or 1", + )), + } + } + + fn visit_i8(self, v: i8) -> Result + where + E: Error, + { + match v { + 0 => Ok(false), + 1 => Ok(true), + unexp => Err(Error::invalid_value( + Unexpected::Signed(unexp as i64), + &"0 or 1", + )), + } + } + + fn visit_u64(self, v: u64) -> Result + where + E: Error, + { + match v { + 0 => Ok(false), + 1 => Ok(true), + unexp => Err(Error::invalid_value(Unexpected::Unsigned(unexp), &"0 or 1")), + } + } + + fn visit_i64(self, v: i64) -> Result + where + E: Error, + { + match v { + 0 => Ok(false), + 1 => Ok(true), + unexp => Err(Error::invalid_value(Unexpected::Signed(unexp), &"0 or 1")), + } + } + + fn visit_u128(self, v: u128) -> Result + where + E: Error, + { + match v { + 0 => Ok(false), + 1 => Ok(true), + unexp => Err(Error::invalid_value( + Unexpected::Unsigned(unexp as u64), + &"0 or 1", + )), + } + } + + fn visit_i128(self, v: i128) -> Result + where + E: Error, + { + match v { + 0 => Ok(false), + 1 => Ok(true), + unexp => Err(Error::invalid_value( + Unexpected::Unsigned(unexp as u64), + &"0 or 1", + )), + } + } + } + + deserializer.deserialize_u8(U8Visitor) + } +} + +impl<'de> DeserializeAs<'de, bool> for BoolFromInt { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct U8Visitor; + impl<'de> Visitor<'de> for U8Visitor { + type Value = bool; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("an integer") + } + + fn visit_u8(self, v: u8) -> Result + where + E: Error, + { + Ok(v != 0) + } + + fn visit_i8(self, v: i8) -> Result + where + E: Error, + { + Ok(v != 0) + } + + fn visit_u64(self, v: u64) -> Result + where + E: Error, + { + Ok(v != 0) + } + + fn visit_i64(self, v: i64) -> Result + where + E: Error, + { + Ok(v != 0) + } + + fn visit_u128(self, v: u128) -> Result + where + E: Error, + { + Ok(v != 0) + } + + fn visit_i128(self, v: i128) -> Result + where + E: Error, + { + Ok(v != 0) + } + } + + deserializer.deserialize_u8(U8Visitor) + } +} + // endregion diff --git a/serde_with/src/lib.rs b/serde_with/src/lib.rs index b72b1fa6..b12f9d86 100644 --- a/serde_with/src/lib.rs +++ b/serde_with/src/lib.rs @@ -1982,3 +1982,52 @@ pub struct BorrowCow; #[derive(Copy, Clone, Debug, Default)] pub struct VecSkipError(PhantomData); + +/// Deserialize a boolean from a number +/// +/// Deserialize a number (of `u8`) and turn it into a boolean. +/// The adapter supports a [`Strict`](crate::formats::Strict) and [`Flexible`](crate::formats::Flexible) format. +/// In `Strict` mode, the number must be `0` or `1`. +/// All other values produce an error. +/// In `Flexible` mode, the number any non-zero value is converted to `true`. +/// +/// During serialization only `0` or `1` are ever emitted. +/// +/// # Examples +/// +/// ```rust +/// # #[cfg(feature = "macros")] { +/// # use serde::{Deserialize, Serialize}; +/// # use serde_json::json; +/// # use serde_with::{serde_as, BoolFromInt}; +/// # +/// #[serde_as] +/// # #[derive(Debug, PartialEq)] +/// #[derive(Deserialize, Serialize)] +/// struct Data(#[serde_as(as = "BoolFromInt")] bool); +/// +/// let data = Data(true); +/// let j = json!(1); +/// // Ensure serialization and deserialization produce the expected results +/// assert_eq!(j, serde_json::to_value(&data).unwrap()); +/// assert_eq!(data, serde_json::from_value(j).unwrap()); +/// +/// // false maps to 0 +/// let data = Data(false); +/// let j = json!(0); +/// assert_eq!(j, serde_json::to_value(&data).unwrap()); +/// assert_eq!(data, serde_json::from_value(j).unwrap()); +// +/// #[serde_as] +/// # #[derive(Debug, PartialEq)] +/// #[derive(Deserialize, Serialize)] +/// struct Flexible(#[serde_as(as = "BoolFromInt")] bool); +/// +/// // Flexible turns any non-zero number into true +/// let data = Flexible(true); +/// let j = json!(100); +/// assert_eq!(data, serde_json::from_value(j).unwrap()); +/// # } +/// ``` +#[derive(Copy, Clone, Debug, Default)] +pub struct BoolFromInt(PhantomData); diff --git a/serde_with/src/ser/impls.rs b/serde_with/src/ser/impls.rs index d170ea78..16a8fb91 100644 --- a/serde_with/src/ser/impls.rs +++ b/serde_with/src/ser/impls.rs @@ -727,4 +727,13 @@ impl<'a> SerializeAs> for BorrowCow { } } +impl SerializeAs for BoolFromInt { + fn serialize_as(source: &bool, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u8(*source as u8) + } +} + // endregion diff --git a/serde_with/tests/serde_as/lib.rs b/serde_with/tests/serde_as/lib.rs index a4dc9d18..0b40ce0c 100644 --- a/serde_with/tests/serde_as/lib.rs +++ b/serde_with/tests/serde_as/lib.rs @@ -30,7 +30,8 @@ use core::cell::{Cell, RefCell}; use expect_test::expect; use serde::{Deserialize, Serialize}; use serde_with::{ - formats::Flexible, serde_as, BytesOrString, CommaSeparator, DisplayFromStr, NoneAsEmptyString, + formats::{Flexible, Strict}, + serde_as, BoolFromInt, BytesOrString, CommaSeparator, DisplayFromStr, NoneAsEmptyString, OneOrMany, Same, StringWithSeparator, }; use std::{ @@ -1059,3 +1060,70 @@ fn test_borrow_cow_str() { let s3 = S3::deserialize(&mut deser).unwrap(); assert!(matches!(s3.0, Cow::Borrowed(_))); } + +#[test] +fn test_boolfromint() { + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct S(#[serde_as(as = "BoolFromInt")] bool); + + is_equal(S(false), expect![[r#"0"#]]); + is_equal(S(true), expect![[r#"1"#]]); + check_error_deserialization::( + "2", + expect![[r#"invalid value: integer `2`, expected 0 or 1 at line 1 column 1"#]], + ); + check_error_deserialization::( + "-100", + expect![[r#"invalid value: integer `-100`, expected 0 or 1 at line 1 column 4"#]], + ); + check_error_deserialization::( + "18446744073709551615", + expect![[ + r#"invalid value: integer `18446744073709551615`, expected 0 or 1 at line 1 column 20"# + ]], + ); + check_error_deserialization::( + r#""""#, + expect![[r#"invalid type: string "", expected an integer 0 or 1 at line 1 column 2"#]], + ); + + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct SStrict(#[serde_as(as = "BoolFromInt")] bool); + + is_equal(SStrict(false), expect![[r#"0"#]]); + is_equal(SStrict(true), expect![[r#"1"#]]); + check_error_deserialization::( + "2", + expect![[r#"invalid value: integer `2`, expected 0 or 1 at line 1 column 1"#]], + ); + check_error_deserialization::( + "-100", + expect![[r#"invalid value: integer `-100`, expected 0 or 1 at line 1 column 4"#]], + ); + check_error_deserialization::( + "18446744073709551615", + expect![[ + r#"invalid value: integer `18446744073709551615`, expected 0 or 1 at line 1 column 20"# + ]], + ); + check_error_deserialization::( + r#""""#, + expect![[r#"invalid type: string "", expected an integer 0 or 1 at line 1 column 2"#]], + ); + + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct SFlexible(#[serde_as(as = "BoolFromInt")] bool); + + is_equal(SFlexible(false), expect![[r#"0"#]]); + is_equal(SFlexible(true), expect![[r#"1"#]]); + check_deserialization::(SFlexible(true), "2"); + check_deserialization::(SFlexible(true), "-100"); + check_deserialization::(SFlexible(true), "18446744073709551615"); + check_error_deserialization::( + r#""""#, + expect![[r#"invalid type: string "", expected an integer at line 1 column 2"#]], + ); +} From 82bd5b860e3bc1ef3e7d49b83e3ea11b9d55b9ba Mon Sep 17 00:00:00 2001 From: Jonas Bushart Date: Sat, 28 May 2022 21:56:46 +0000 Subject: [PATCH 2/2] Add Changelog and guide entry --- serde_with/CHANGELOG.md | 17 +++++- .../src/guide/serde_as_transformations.md | 59 ++++++++++++------- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/serde_with/CHANGELOG.md b/serde_with/CHANGELOG.md index ec98e2a7..b8d2347a 100644 --- a/serde_with/CHANGELOG.md +++ b/serde_with/CHANGELOG.md @@ -24,7 +24,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. `time::OffsetDateTime` and `time::PrimitiveDateTime` can now be serialized with the `TimestampSeconds` and related converters. - ```rust // Rust #[serde_as(as = "serde_with::TimestampMicroSecondsWithFrac")] @@ -49,6 +48,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. "rfc_3339": ["1997-11-21T09:55:06-06:00"], ``` +* Deserialize `bool` from integers #456 462 + + Deserialize an integer and convert it into a `bool`. + `BoolFromInt` (default) deserializes 0 to `false` and `1` to `true`, other numbers are errors. + `BoolFromInt` deserializes any non-zero as `true`. + Serialization only emits 0/1. + + ```rust + // Rust + #[serde_as(as = "BoolFromInt")] // BoolFromInt + b: bool, + + // JSON + "b": 1, + ``` + ### Changed * Bump MSRV to 1.53, since the new dependency `time` requires that version. diff --git a/serde_with/src/guide/serde_as_transformations.md b/serde_with/src/guide/serde_as_transformations.md index d553920a..9be67e15 100644 --- a/serde_with/src/guide/serde_as_transformations.md +++ b/serde_with/src/guide/serde_as_transformations.md @@ -4,26 +4,27 @@ This page lists the transformations implemented in this crate and supported by ` 1. [Base64 encode bytes](#base64-encode-bytes) 2. [Big Array support](#big-array-support) -3. [Borrow from the input for `Cow` type](#borrow-from-the-input-for-cow-type) -4. [`Bytes` with more efficiency](#bytes-with-more-efficiency) -5. [Convert to an intermediate type using `Into`](#convert-to-an-intermediate-type-using-into) -6. [Convert to an intermediate type using `TryInto`](#convert-to-an-intermediate-type-using-tryinto) -7. [`Default` from `null`](#default-from-null) -8. [De/Serialize into `Vec`, ignoring errors](#deserialize-into-vec-ignoring-errors) -9. [De/Serialize with `FromStr` and `Display`](#deserialize-with-fromstr-and-display) -10. [`Duration` as seconds](#duration-as-seconds) -11. [Hex encode bytes](#hex-encode-bytes) -12. [Ignore deserialization errors](#ignore-deserialization-errors) -13. [`Maps` to `Vec` of enums](#maps-to-vec-of-enums) -14. [`Maps` to `Vec` of tuples](#maps-to-vec-of-tuples) -15. [`NaiveDateTime` like UTC timestamp](#naivedatetime-like-utc-timestamp) -16. [`None` as empty `String`](#none-as-empty-string) -17. [One or many elements into `Vec`](#one-or-many-elements-into-vec) -18. [Pick first successful deserialization](#pick-first-successful-deserialization) -19. [Timestamps as seconds since UNIX epoch](#timestamps-as-seconds-since-unix-epoch) -20. [Value into JSON String](#value-into-json-string) -21. [`Vec` of tuples to `Maps`](#vec-of-tuples-to-maps) -22. [Well-known time formats for `OffsetDateTime`](#well-known-time-formats-for-offsetdatetime) +3. [`bool` from integer](#bool-from-integer) +4. [Borrow from the input for `Cow` type](#borrow-from-the-input-for-cow-type) +5. [`Bytes` with more efficiency](#bytes-with-more-efficiency) +6. [Convert to an intermediate type using `Into`](#convert-to-an-intermediate-type-using-into) +7. [Convert to an intermediate type using `TryInto`](#convert-to-an-intermediate-type-using-tryinto) +8. [`Default` from `null`](#default-from-null) +9. [De/Serialize into `Vec`, ignoring errors](#deserialize-into-vec-ignoring-errors) +10. [De/Serialize with `FromStr` and `Display`](#deserialize-with-fromstr-and-display) +11. [`Duration` as seconds](#duration-as-seconds) +12. [Hex encode bytes](#hex-encode-bytes) +13. [Ignore deserialization errors](#ignore-deserialization-errors) +14. [`Maps` to `Vec` of enums](#maps-to-vec-of-enums) +15. [`Maps` to `Vec` of tuples](#maps-to-vec-of-tuples) +16. [`NaiveDateTime` like UTC timestamp](#naivedatetime-like-utc-timestamp) +17. [`None` as empty `String`](#none-as-empty-string) +18. [One or many elements into `Vec`](#one-or-many-elements-into-vec) +19. [Pick first successful deserialization](#pick-first-successful-deserialization) +20. [Timestamps as seconds since UNIX epoch](#timestamps-as-seconds-since-unix-epoch) +21. [Value into JSON String](#value-into-json-string) +22. [`Vec` of tuples to `Maps`](#vec-of-tuples-to-maps) +23. [Well-known time formats for `OffsetDateTime`](#well-known-time-formats-for-offsetdatetime) ## Base64 encode bytes @@ -57,6 +58,22 @@ value: [[u8; 64]; 33], "value": [[0,0,0,0,0,...], [0,0,0,...], ...], ``` +## `bool` from integer + +Deserialize an integer and convert it into a `bool`. +[`BoolFromInt`] (default) deserializes 0 to `false` and `1` to `true`, other numbers are errors. +[`BoolFromInt`] deserializes any non-zero as `true`. +Serialization only emits 0/1. + +```ignore +// Rust +#[serde_as(as = "BoolFromInt")] // BoolFromInt +b: bool, + +// JSON +"b": 1, +``` + ## Borrow from the input for `Cow` type The types `Cow<'_, str>`, `Cow<'_, [u8]>`, or `Cow<'_, [u8; N]>` can borrow from the input, avoiding extra copies. @@ -471,6 +488,8 @@ rfc_3339: OffsetDateTime, These conversions are availble with the `time_0_3` feature flag. [`Base64`]: crate::base64::Base64 +[`BoolFromInt`]: crate::BoolFromInt +[`BoolFromInt`]: crate::BoolFromInt [`Bytes`]: crate::Bytes [`chrono::DateTime`]: chrono_crate::DateTime [`chrono::DateTime`]: chrono_crate::DateTime