Skip to content
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

Support BigInt for i64 and u64 #24

Merged
merged 19 commits into from
Dec 24, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 0 additions & 18 deletions src/big-int-checks.js

This file was deleted.

15 changes: 7 additions & 8 deletions src/bindings.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
use js_sys::BigInt;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(module = "/src/big-int-checks.js")]
#[wasm_bindgen]
extern "C" {
pub fn to_u64(x: &BigInt) -> Option<u64>;
#[wasm_bindgen(js_name = BigInt)]
pub fn big_int_to_u64(x: &BigInt) -> u64;

pub fn to_i64(x: &BigInt) -> Option<i64>;
}
#[wasm_bindgen(js_name = BigInt)]
pub fn big_int_to_i64(x: &BigInt) -> i64;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = BigInt)]
pub fn from_u64(x: u64) -> BigInt;
pub fn big_int_from_u64(x: u64) -> BigInt;

#[wasm_bindgen(js_name = BigInt)]
pub fn from_i64(x: i64) -> BigInt;
pub fn big_int_from_i64(x: i64) -> BigInt;
}
20 changes: 14 additions & 6 deletions src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ impl<'de> de::Deserializer<'de> for Deserializer {
self.deserialize_from_js_number_signed(visitor)
}

// TODO: Add i128 deserializer rather than forwarding to i64
serde_if_integer128! {
fn deserialize_i128<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
self.deserialize_i64(visitor)
Expand All @@ -363,6 +364,7 @@ impl<'de> de::Deserializer<'de> for Deserializer {
self.deserialize_from_js_number_unsigned(visitor)
}

// TODO: Add u128 deserializer rather than forwarding to u64
serde_if_integer128! {
fn deserialize_u128<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
self.deserialize_u64(visitor)
Expand All @@ -371,9 +373,12 @@ impl<'de> de::Deserializer<'de> for Deserializer {

fn deserialize_i64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
if let Some(big_int) = self.value.dyn_ref::<BigInt>() {
match bindings::to_i64(big_int) {
Some(value) => visitor.visit_i64(value),
None => Err(de::Error::custom("i64 attempted to be constructed from Bigint that was larger than i64::MAX or less than i64::MIN"))
let converted_number = bindings::big_int_to_i64(big_int);
// Do a round trip check in order to make sure that no information was lost
if &bindings::big_int_from_i64(converted_number) == big_int {
visitor.visit_i64(converted_number)
} else {
Err(de::Error::custom("i64 attempted to be constructed from Bigint that was larger than i64::MAX or less than i64::MIN"))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it's BigInt (same in the other message below).

}
} else {
self.deserialize_from_js_number_signed(visitor)
Expand All @@ -382,9 +387,12 @@ impl<'de> de::Deserializer<'de> for Deserializer {

fn deserialize_u64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
if let Some(big_int) = self.value.dyn_ref::<BigInt>() {
match bindings::to_u64(big_int) {
Some(value) => visitor.visit_u64(value),
None => Err(de::Error::custom("u64 attempted to be constructed from Bigint that was either larger than u64::MAX or less than u64::MIN"))
let converted_number = bindings::big_int_to_u64(big_int);
// Do a round trip check in order to make sure that no information was lost
if &bindings::big_int_from_u64(converted_number) == big_int {
visitor.visit_u64(converted_number)
} else {
Err(de::Error::custom("u64 attempted to be constructed from Bigint that was either larger than u64::MAX or less than u64::MIN"))
}
} else {
self.deserialize_from_js_number_unsigned(visitor)
Expand Down
31 changes: 24 additions & 7 deletions src/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ impl ser::SerializeStruct for ObjectSerializer<'_> {
#[derive(Default)]
pub struct Serializer {
serialize_maps_as_objects: bool,
serialize_64_bit_numbers_as_big_int: bool,
serialize_large_number_types_as_big_ints: bool,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also nitpicking, but JS uses bigint for primitive type, so I think we should write them together in the API too.

}

impl Serializer {
Expand All @@ -233,8 +233,8 @@ impl Serializer {

/// Set to `true` to serialize 64 bit numbers to JavaScript `BigInt` instead of
/// `Number`. `false` by default
pub fn serialize_64_bit_numbers_as_big_int(mut self, value: bool) -> Self {
self.serialize_64_bit_numbers_as_big_int = value;
pub fn serialize_large_number_types_as_big_ints(mut self, value: bool) -> Self {
self.serialize_large_number_types_as_big_ints = value;
self
}
}
Expand Down Expand Up @@ -277,8 +277,8 @@ impl<'s> ser::Serializer for &'s Serializer {
}

fn serialize_i64(self, v: i64) -> Result {
if self.serialize_64_bit_numbers_as_big_int {
return Ok(bindings::from_i64(v).into());
if self.serialize_large_number_types_as_big_ints {
return Ok(bindings::big_int_from_i64(v).into());
}
const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
const MIN_SAFE_INTEGER: i64 = -MAX_SAFE_INTEGER;
Expand All @@ -292,9 +292,10 @@ impl<'s> ser::Serializer for &'s Serializer {
)))
}
}

fn serialize_u64(self, v: u64) -> Result {
if self.serialize_64_bit_numbers_as_big_int {
return Ok(bindings::from_u64(v).into());
if self.serialize_large_number_types_as_big_ints {
return Ok(bindings::big_int_from_u64(v).into());
}

const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
Expand All @@ -309,6 +310,22 @@ impl<'s> ser::Serializer for &'s Serializer {
}
}

fn serialize_i128(self, v: i128) -> Result {
if self.serialize_large_number_types_as_big_ints {
Ok(JsValue::from(v))
} else {
Err(Error::custom("i128 is not supported"))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, shouldn't this fallback to JS number when option isn't activated? I'm of two minds on this, perhaps it's fine for it to error out, but then it would be good to at least expand the error message to clarify that it's supported, just needs to be enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The current behavior is to error so I was going to uphold that. In addition, I don't think there are many cases where it would be a good idea to convert a 128-bit integer to a 53 bit one (Js Number).

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah erroring is fine if that's current behaviour, just saying the error message probably needs to be clearer and nudge the user in the right direction.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even something like "i128 is supported only in serialize_large_number_types_as_big_ints mode" would already go a long way.

Copy link
Contributor Author

@mfish33 mfish33 Dec 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad fixed the error in one place and not the other. It is updated now

}
}

fn serialize_u128(self, v: u128) -> Result {
if self.serialize_large_number_types_as_big_ints {
Ok(JsValue::from(v))
} else {
Err(Error::custom("u128 is not supported"))
}
}

fn serialize_char(self, v: char) -> Result {
Ok(JsString::from(v).into())
}
Expand Down
75 changes: 73 additions & 2 deletions tests/serde.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use js_sys::BigInt;
use maplit::{btreemap, hashmap, hashset};
use serde::de::DeserializeOwned;
use serde::ser::Error as SerError;
Expand Down Expand Up @@ -180,11 +181,33 @@ fn numbers() {
to_value(&std::u64::MAX).unwrap_err();
}

// Test u64 and i64 bigint serialization feature
let big_int_serializer = Serializer::new().serialize_64_bit_numbers_as_big_int(true);
// By default serializing i128 and u128 results in an error
{
to_value(&0_i128).unwrap_err();
to_value(&0_u128).unwrap_err();
}

// By default deserializing i128 and u128 uses 64 bit implementation
{
assert_eq!(from_value::<i128>(JsValue::from(0_i128)).unwrap(), 0);
assert_eq!(from_value::<i128>(JsValue::from(42_i128)).unwrap(), 42);
assert_eq!(from_value::<i128>(JsValue::from(-42_i128)).unwrap(), -42);
assert_eq!(from_value::<u128>(JsValue::from(0_u128)).unwrap(), 0);
assert_eq!(from_value::<u128>(JsValue::from(42_u128)).unwrap(), 42);
}

// Test large number bigint serialization feature
let big_int_serializer = Serializer::new().serialize_large_number_types_as_big_ints(true);
{
const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;

// Should be bigint
0_i64
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: instead of converting, you can probably just do assert!(expr.is_bigint()). Or does this one provide a better error message or something?

.serialize(&big_int_serializer)
.unwrap()
.dyn_into::<BigInt>()
.unwrap();

// u64 and i64 should serialize the same
test_via_into_with_config(0_i64, 0_u64, &big_int_serializer);
test_via_into_with_config(42_i64, 42_u64, &big_int_serializer);
Expand Down Expand Up @@ -232,6 +255,13 @@ fn numbers() {
{
const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;

// Should be bigint
0_u64
.serialize(&big_int_serializer)
.unwrap()
.dyn_into::<BigInt>()
.unwrap();

// u64 and i64 should serialize the same
test_via_into_with_config(0_u64, 0_i64, &big_int_serializer);
test_via_into_with_config(42_u64, 42_i64, &big_int_serializer);
Expand Down Expand Up @@ -267,6 +297,47 @@ fn numbers() {
JsValue::from(std::u64::MAX)
);
}

// i128 and u128 should serialize to bigint when the feature is enabled
{
// Should be bigint
0_i128
.serialize(&big_int_serializer)
.unwrap()
.dyn_into::<BigInt>()
.unwrap();

// i128 and u128 should serialize the same
test_via_into_with_config(0_u128, 0_i128, &big_int_serializer);
test_via_into_with_config(42_u128, 42_i128, &big_int_serializer);

// Can still deserialize from JS numbers
assert_eq!(from_value::<i128>(JsValue::from_f64(1.0)).unwrap(), 1);

// Invalid floats should fail
from_value::<i128>(JsValue::from_f64(1.5)).unwrap_err();
from_value::<i128>(JsValue::from_f64(-10.2)).unwrap_err();
}

{
// Should be bigint
0_u128
.serialize(&big_int_serializer)
.unwrap()
.dyn_into::<BigInt>()
.unwrap();

// i128 and u128 should serialize the same
test_via_into_with_config(0_i128, 0_u128, &big_int_serializer);
test_via_into_with_config(42_i128, 42_u128, &big_int_serializer);

// Can still deserialize from JS numbers
assert_eq!(from_value::<u128>(JsValue::from_f64(1.0)).unwrap(), 1);

// Invalid floats should fail
from_value::<u128>(JsValue::from_f64(1.5)).unwrap_err();
from_value::<u128>(JsValue::from_f64(-10.2)).unwrap_err();
}
}

#[wasm_bindgen_test]
Expand Down