Skip to content

Replace float_deserialize feature with deserialize_via_float method #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

* Breaking: Allow values larger than 32 bit for `int!` and `uint!` macros
This means that `i32`-suffixed and `u32`-suffixed literals are no longer accepcted
* The old macros can be replaced like follows: `int!(42)` -> `Int::from(42_i32)` etc.
* The old macros can be replaced like follows: `int!(42)` -> `Int::from(42_i32)` etc.
* The minimum supported rust version is raised to 1.46.
* The `int!` and `uint!` macros now support arbitrary const expressions, not just literals
* `Int::new` and `UInt::new` are now const
* Breaking: Serialization of `Int` and `UInt` now call the serialization of `i64` and `u64` directly instead of
serializing them as newtype structs, emulating `#[serde(transparent)]`.
This doesn't make a difference for `serde_json` for example, but it could make a difference for other serializers
* Breaking: Remove the `float_deserialize` feature and replace it with the `(U)Int::deserialize_via_float` methods that
can be used with serde's `deserialize_with` attribute.

# 0.2.2

Expand All @@ -32,7 +34,7 @@
# 0.1.9

* Add a new Cargo feature: `lax_deserialize`
* See the crate documentation or [README.md](README.md) for what it does.
* See the crate documentation or [README.md](README.md) for what it does.

# 0.1.8

Expand Down
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ optional = true
default-features = false

[features]
float_deserialize = ["serde"]
default = ["std"]
"rust_1.81" = []
std = []
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ implementations of `std::error::Error` for `ParseIntError` and
without the `std` feature.

Deserialization can be routed through `f64` instead of `u64` with the
`float_deserialize` feature. This will still not deserialize numbers with a
`(U)Int::deserialize_via_float` methods. This will still not deserialize numbers with a
non-zero fractional component. Please be aware that
`serde_json` doesn't losslessly parse large floats with a fractional part by
default (even if the fractional part is `.0`). To fix that, enable its
Expand Down
72 changes: 45 additions & 27 deletions src/int.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,44 @@ impl Int {
Self::new_saturating(self.0.saturating_pow(exp))
}

/// Deserialization function for use with `#[serde(deserialize_with = ...)]` that performs
/// deserialization through `f64` instead of `i64`.
/// This allows deserializing from numbers with a fractional component like `.0`.
///
/// Note, however, that this will not accept non-zero fractional components like `.1`.
///
/// # Example
/// ```rs
/// use serde::Deserialize;
/// use js_int::Int;
///
/// #[derive(Deserialize)]
/// struct Point {
/// #[serde(deserialize_with = "Int::deserialize_via_float")]
/// x: Int;
/// #[serde(deserialize_with = "Int::deserialize_via_float")]
/// y: Int;
/// }
#[cfg(feature = "serde")]
pub fn deserialize_via_float<'de, D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
const EXPECTING: &str =
"a number between -2^53 + 1 and 2^53 - 1 without fractional component";

let val = f64::deserialize(deserializer)?;

if val > MAX_SAFE_INT as f64
|| val < MIN_SAFE_INT as f64
|| !super::is_acceptable_float(val)
{
Err(D::Error::invalid_value(Unexpected::Float(val), &EXPECTING))
} else {
Ok(Self(val as i64))
}
}

// TODO: wrapping_* methods, overflowing_* methods
}

Expand Down Expand Up @@ -576,34 +614,14 @@ impl<'de> Deserialize<'de> for Int {
where
D: Deserializer<'de>,
{
#[cfg(not(feature = "float_deserialize"))]
{
let val = i64::deserialize(deserializer)?;

Self::new(val).ok_or_else(|| {
D::Error::invalid_value(
Unexpected::Signed(val),
&"an integer between -2^53 + 1 and 2^53 - 1",
)
})
}

#[cfg(feature = "float_deserialize")]
{
const EXPECTING: &str =
"a number between -2^53 + 1 and 2^53 - 1 without fractional component";
let val = i64::deserialize(deserializer)?;

let val = f64::deserialize(deserializer)?;

if val > MAX_SAFE_INT as f64
|| val < MIN_SAFE_INT as f64
|| !super::is_acceptable_float(val)
{
Err(D::Error::invalid_value(Unexpected::Float(val), &EXPECTING))
} else {
Ok(Self(val as i64))
}
}
Self::new(val).ok_or_else(|| {
D::Error::invalid_value(
Unexpected::Signed(val),
&"an integer between -2^53 + 1 and 2^53 - 1",
)
})
}
}

Expand Down
5 changes: 1 addition & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@
//! * `serde`: Serialization and deserialization support via [serde](https://serde.rs). Disabled by
//! default. You can use `js_int` + `serde` in `#![no_std]` contexts if you use
//! `default-features = false` for both.
//! * `float_deserialize`: Deserialize via `f64`, not via `u64`. If the input has a fraction,
//! deserialization will fail.
//! * `lax_deserialize`: Like `float_deserialize`, but if the input has a fraction, it is
//! deserialized with the fractional part discarded.
//! Please be aware that `serde_json` doesn't losslessly parse large floats with a fractional part
//! by default (even if the fractional part is `.0`). To fix that, enable its `float_roundtrip`
Expand Down Expand Up @@ -59,7 +56,7 @@ pub use self::{
uint::{UInt, MAX_SAFE_UINT},
};

#[cfg(feature = "float_deserialize")]
#[cfg(feature = "serde")]
#[inline(always)]
pub(crate) fn is_acceptable_float(float: f64) -> bool {
!float.is_nan() && float.fract() == 0.0
Expand Down
62 changes: 39 additions & 23 deletions src/uint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,41 @@ impl UInt {
Self::new_saturating(self.0.saturating_pow(exp))
}

/// Deserialization function for use with `#[serde(deserialize_with = ...)]` that performs
/// deserialization through `f64` instead of `u64`.
/// This allows deserializing from numbers with a fractional component like `.0`.
///
/// Note, however, that this will not accept non-zero fractional components like `.1`.
///
/// # Example
/// ```rs
/// use serde::Deserialize;
/// use js_int::UInt;
///
/// #[derive(Deserialize)]
/// struct Point {
/// #[serde(deserialize_with = "UInt::deserialize_via_float")]
/// x: UInt;
/// #[serde(deserialize_with = "UInt::deserialize_via_float")]
/// y: UInt;
/// }
/// ```
#[cfg(feature = "serde")]
pub fn deserialize_via_float<'de, D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
const EXPECTING: &str = "a number between 0 and 2^53 - 1 without fractional component";

let val = f64::deserialize(deserializer)?;

if val < 0.0 || val > MAX_SAFE_UINT as f64 || !super::is_acceptable_float(val) {
Err(D::Error::invalid_value(Unexpected::Float(val), &EXPECTING))
} else {
Ok(Self(val as u64))
}
}

// TODO: wrapping_* methods, overflowing_* methods
}

Expand Down Expand Up @@ -571,30 +606,11 @@ impl<'de> Deserialize<'de> for UInt {
where
D: Deserializer<'de>,
{
#[cfg(not(feature = "float_deserialize"))]
{
let val = u64::deserialize(deserializer)?;

Self::new(val).ok_or_else(|| {
D::Error::invalid_value(
Unexpected::Unsigned(val),
&"an integer between 0 and 2^53 - 1",
)
})
}

#[cfg(feature = "float_deserialize")]
{
const EXPECTING: &str = "a number between 0 and 2^53 - 1 without fractional component";
let val = u64::deserialize(deserializer)?;

let val = f64::deserialize(deserializer)?;

if val < 0.0 || val > MAX_SAFE_UINT as f64 || !super::is_acceptable_float(val) {
Err(D::Error::invalid_value(Unexpected::Float(val), &EXPECTING))
} else {
Ok(Self(val as u64))
}
}
Self::new(val).ok_or_else(|| {
D::Error::invalid_value(Unexpected::Unsigned(val), &"an integer between 0 and 2^53 - 1")
})
}
}

Expand Down
26 changes: 15 additions & 11 deletions tests/int.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ fn deserialize() {
}

#[test]
#[cfg_attr(feature = "float_deserialize", ignore)]
fn dont_deserialize_integral_float() {
assert!(deserialize_from(-10.0).is_err());
assert!(deserialize_from(-0.0).is_err());
Expand All @@ -49,22 +48,27 @@ fn dont_deserialize_fractional_float() {
}

#[test]
#[cfg_attr(not(feature = "float_deserialize"), ignore)]
fn deserialize_integral_float() {
assert_eq!(deserialize_from(-10.0).unwrap(), int!(-10));
assert_eq!(deserialize_from(-0.0).unwrap(), int!(0));
assert_eq!(deserialize_from(1.0).unwrap(), int!(1));
assert_eq!(deserialize_from(9007199254740991.0).unwrap(), Int::MAX);
assert!(deserialize_from(9007199254740992.0).is_err());
assert_eq!(deserialize_via_float(-10.0).unwrap(), int!(-10));
assert_eq!(deserialize_via_float(-0.0).unwrap(), int!(0));
assert_eq!(deserialize_via_float(1.0).unwrap(), int!(1));
assert_eq!(deserialize_via_float(9007199254740991.0).unwrap(), Int::MAX);
assert!(deserialize_via_float(9007199254740992.0).is_err());
// NOTE: This still ends up as integral because the .49 exceeds the representable range of f64
assert_eq!(
deserialize_from(9007199254740991.49).unwrap(),
deserialize_via_float(9007199254740991.49).unwrap(),
Int::try_from(9007199254740991i64).unwrap()
);

assert!(deserialize_from(f64::NAN).is_err());
assert!(deserialize_from(f64::INFINITY).is_err());
assert!(deserialize_from(f64::NEG_INFINITY).is_err());
assert!(deserialize_via_float(f64::NAN).is_err());
assert!(deserialize_via_float(f64::INFINITY).is_err());
assert!(deserialize_via_float(f64::NEG_INFINITY).is_err());

fn deserialize_via_float<'de, Value: IntoDeserializer<'de>>(
value: Value,
) -> Result<Int, serde::de::value::Error> {
Int::deserialize_via_float(value.into_deserializer())
}
}

fn deserialize_from<'de, Value: IntoDeserializer<'de>>(
Expand Down
22 changes: 13 additions & 9 deletions tests/uint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ fn deserialize() {
}

#[test]
#[cfg_attr(feature = "float_deserialize", ignore)]
fn dont_deserialize_integral_float() {
assert!(deserialize_from(1.0).is_err());
assert!(deserialize_from(9007199254740991.0).is_err());
Expand All @@ -43,20 +42,25 @@ fn dont_deserialize_fractional_float() {
}

#[test]
#[cfg_attr(not(feature = "float_deserialize"), ignore)]
fn deserialize_integral_float() {
assert_eq!(deserialize_from(1.0).unwrap(), uint!(1));
assert_eq!(deserialize_from(9007199254740991.0).unwrap(), UInt::MAX);
assert!(deserialize_from(9007199254740992.0).is_err());
assert_eq!(deserialize_via_float(1.0).unwrap(), uint!(1));
assert_eq!(deserialize_via_float(9007199254740991.0).unwrap(), UInt::MAX);
assert!(deserialize_via_float(9007199254740992.0).is_err());
// NOTE: This still ends up as integral because the .49 exceeds the representable range of f64
assert_eq!(
deserialize_from(9007199254740991.49).unwrap(),
deserialize_via_float(9007199254740991.49).unwrap(),
UInt::try_from(9007199254740991i64).unwrap()
);

assert!(deserialize_from(f64::NAN).is_err());
assert!(deserialize_from(f64::INFINITY).is_err());
assert!(deserialize_from(f64::NEG_INFINITY).is_err());
assert!(deserialize_via_float(f64::NAN).is_err());
assert!(deserialize_via_float(f64::INFINITY).is_err());
assert!(deserialize_via_float(f64::NEG_INFINITY).is_err());

fn deserialize_via_float<'de, Value: IntoDeserializer<'de>>(
value: Value,
) -> Result<UInt, serde::de::value::Error> {
UInt::deserialize_via_float(value.into_deserializer())
}
}

fn deserialize_from<'de, Value: IntoDeserializer<'de>>(
Expand Down