Skip to content

Commit b627d82

Browse files
andoriyuranile
andauthored
feat(gloo-utils): Lift serde-serialization from wasm-bindgen (#242)
* feat(gloo-utils): Lift serde-serialization from wasm-bindgen * Update crates/utils/Cargo.toml Co-authored-by: Muhammad Hamza <[email protected]> * Update crates/utils/src/json.rs Co-authored-by: Muhammad Hamza <[email protected]> * Update crates/utils/src/json.rs Co-authored-by: Muhammad Hamza <[email protected]> * Address the feedback * only re-export if feature is enable * address the feedback * Update json.rs * Update json.rs * Update json.rs * cargo fmt Co-authored-by: Muhammad Hamza <[email protected]>
1 parent 24d2f2c commit b627d82

File tree

10 files changed

+239
-23
lines changed

10 files changed

+239
-23
lines changed

crates/console/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ categories = ["api-bindings", "development-tools::profiling", "wasm"]
1515
wasm-bindgen = "0.2"
1616
js-sys = "0.3"
1717
serde = { version = "1", features = ["derive"] }
18-
serde_json = "1.0"
18+
gloo-utils = { version = "0.1", path = "../utils", features = ["serde"] }
1919
[dependencies.web-sys]
2020
version = "0.3"
2121
features = [

crates/console/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub use timer::Timer;
3030

3131
#[doc(hidden)]
3232
pub mod __macro {
33+
use gloo_utils::format::JsValueSerdeExt;
3334
pub use js_sys::Array;
3435
pub use wasm_bindgen::JsValue;
3536
use wasm_bindgen::UnwrapThrowExt;
@@ -38,7 +39,7 @@ pub mod __macro {
3839
data: impl serde::Serialize,
3940
columns: impl IntoIterator<Item = &'a str>,
4041
) {
41-
let data = js_sys::JSON::parse(&serde_json::to_string(&data).unwrap_throw()).unwrap_throw();
42+
let data = <JsValue as JsValueSerdeExt>::from_serde(&data).unwrap_throw();
4243
let columns = columns.into_iter().map(JsValue::from_str).collect();
4344

4445
crate::externs::table_with_data_and_columns(data, columns);

crates/net/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ rustdoc-args = ["--cfg", "docsrs"]
1818
wasm-bindgen = "0.2"
1919
web-sys = "0.3"
2020
js-sys = "0.3"
21-
gloo-utils = { version = "0.1", path = "../utils" }
21+
gloo-utils = { version = "0.1", path = "../utils", features = ["serde"] }
2222

2323
wasm-bindgen-futures = "0.4"
2424
futures-core = { version = "0.3", optional = true }

crates/net/src/http/mod.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ use wasm_bindgen_futures::JsFuture;
2727
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
2828
use serde::de::DeserializeOwned;
2929

30+
#[cfg(feature = "json")]
31+
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
32+
use gloo_utils::format::JsValueSerdeExt;
33+
3034
pub use headers::Headers;
3135
pub use query::QueryParams;
3236
pub use web_sys::{
@@ -391,16 +395,8 @@ impl Response {
391395
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
392396
pub async fn json<T: DeserializeOwned>(&self) -> Result<T, Error> {
393397
let promise = self.response.json().map_err(js_to_error)?;
394-
let json = JsFuture::from(promise)
395-
.await
396-
.map_err(js_to_error)
397-
.and_then(|json| {
398-
js_sys::JSON::stringify(&json)
399-
.map(String::from)
400-
.map_err(js_to_error)
401-
})?;
402-
403-
Ok(serde_json::from_str(&json)?)
398+
let json = JsFuture::from(promise).await.map_err(js_to_error)?;
399+
Ok(JsValueSerdeExt::into_serde(&json)?)
404400
}
405401

406402
/// Reads the response as a String.

crates/net/src/websocket/futures.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,13 @@ impl WebSocket {
106106
url: &str,
107107
protocols: &[S],
108108
) -> Result<Self, JsError> {
109-
let map_err = |err| {
110-
js_sys::Error::new(&format!(
111-
"Failed to convert protocols to Javascript value: {}",
112-
err
113-
))
114-
};
115-
let json = serde_json::to_string(protocols)
116-
.map_err(|e| map_err(e.to_string()))
117-
.map(|d| js_sys::JSON::parse(&d).unwrap_throw())?;
118-
109+
let json = <JsValue as gloo_utils::format::JsValueSerdeExt>::from_serde(protocols)
110+
.map_err(|err| {
111+
js_sys::Error::new(&format!(
112+
"Failed to convert protocols to Javascript value: {}",
113+
err
114+
))
115+
})?;
119116
Self::setup(web_sys::WebSocket::new_with_str_sequence(url, &json))
120117
}
121118

crates/utils/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ categories = ["api-bindings", "wasm"]
1414
[dependencies]
1515
wasm-bindgen = "0.2"
1616
js-sys = "0.3"
17+
serde = { version = "1.0", optional = true }
18+
serde_json = {version = "1.0", optional = true }
1719

1820
[dependencies.web-sys]
1921
version = "0.3"
@@ -27,5 +29,14 @@ features = [
2729
"Element",
2830
]
2931

32+
[features]
33+
default = ["serde"]
34+
serde = ["dep:serde", "dep:serde_json"]
35+
3036
[dev-dependencies]
3137
wasm-bindgen-test = "0.3"
38+
serde_derive = "1.0"
39+
40+
[package.metadata.docs.rs]
41+
all-features = true
42+
rustdoc-args = ["--cfg", "docsrs"]

crates/utils/src/format/json.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#![cfg(feature = "serde")]
2+
3+
use wasm_bindgen::{JsValue, UnwrapThrowExt};
4+
mod private {
5+
pub trait Sealed {}
6+
impl Sealed for wasm_bindgen::JsValue {}
7+
}
8+
9+
/// Extenstion trait to provide conversion between [`JsValue`](wasm_bindgen::JsValue) and [`serde`].
10+
///
11+
/// Usage of this API requires activating the `serde` feature of the `gloo-utils` crate.
12+
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
13+
pub trait JsValueSerdeExt: private::Sealed {
14+
/// Creates a new `JsValue` from the JSON serialization of the object `t`
15+
/// provided.
16+
///
17+
/// This function will serialize the provided value `t` to a JSON string,
18+
/// send the JSON string to JS, parse it into a JS object, and then return
19+
/// a handle to the JS object. This is unlikely to be super speedy so it's
20+
/// not recommended for large payloads, but it's a nice to have in some
21+
/// situations!
22+
///
23+
/// Usage of this API requires activating the `serde` feature of
24+
/// the `gloo-utils` crate.
25+
/// # Example
26+
///
27+
/// ```rust
28+
/// use wasm_bindgen::JsValue;
29+
/// use gloo_utils::format::JsValueSerdeExt;
30+
///
31+
/// # fn no_run() {
32+
/// assert_eq!(JsValue::from("bar").into_serde::<String>().unwrap(), "bar");
33+
/// # }
34+
/// ```
35+
/// # Errors
36+
///
37+
/// Returns any error encountered when serializing `T` into JSON.
38+
///
39+
/// # Panics
40+
///
41+
/// Panics if [`serde_json`](serde_json::to_string) generated JSON that couldn't be parsed by [`js_sys`].
42+
/// Uses [`unwrap_throw`](UnwrapThrowExt::unwrap_throw) from [`wasm_bindgen::UnwrapThrowExt`].
43+
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
44+
fn from_serde<T>(t: &T) -> serde_json::Result<JsValue>
45+
where
46+
T: serde::ser::Serialize + ?Sized;
47+
48+
/// Invokes `JSON.stringify` on this value and then parses the resulting
49+
/// JSON into an arbitrary Rust value.
50+
///
51+
/// This function will first call `JSON.stringify` on the `JsValue` itself.
52+
/// The resulting string is then passed into Rust which then parses it as
53+
/// JSON into the resulting value. If given `undefined`, object will be silentrly changed to
54+
/// null to avoid panic.
55+
///
56+
/// Usage of this API requires activating the `serde` feature of
57+
/// the `gloo-utils` crate.
58+
///
59+
/// # Example
60+
///
61+
/// ```rust
62+
/// use wasm_bindgen::JsValue;
63+
/// use gloo_utils::format::JsValueSerdeExt;
64+
///
65+
/// # fn no_run() {
66+
/// let array = vec![1,2,3];
67+
/// let obj = JsValue::from_serde(&array);
68+
/// # }
69+
/// ```
70+
///
71+
/// # Errors
72+
///
73+
/// Returns any error encountered when parsing the JSON into a `T`.
74+
///
75+
/// # Panics
76+
///
77+
/// Panics if [`js_sys`] couldn't stringify the JsValue. Uses [`unwrap_throw`](UnwrapThrowExt::unwrap_throw)
78+
/// from [`wasm_bindgen::UnwrapThrowExt`].
79+
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
80+
#[allow(clippy::wrong_self_convention)]
81+
fn into_serde<T>(&self) -> serde_json::Result<T>
82+
where
83+
T: for<'a> serde::de::Deserialize<'a>;
84+
}
85+
86+
impl JsValueSerdeExt for JsValue {
87+
fn from_serde<T>(t: &T) -> serde_json::Result<JsValue>
88+
where
89+
T: serde::ser::Serialize + ?Sized,
90+
{
91+
let s = serde_json::to_string(t)?;
92+
Ok(js_sys::JSON::parse(&s).unwrap_throw())
93+
}
94+
95+
fn into_serde<T>(&self) -> serde_json::Result<T>
96+
where
97+
T: for<'a> serde::de::Deserialize<'a>,
98+
{
99+
// Turns out `JSON.stringify(undefined) === undefined`, so if
100+
// we're passed `undefined` reinterpret it as `null` for JSON
101+
// purposes.
102+
let s = if self.is_undefined() {
103+
String::from("null")
104+
} else {
105+
js_sys::JSON::stringify(self)
106+
.map(String::from)
107+
.unwrap_throw()
108+
};
109+
serde_json::from_str(&s)
110+
}
111+
}

crates/utils/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
#![cfg_attr(docsrs, feature(doc_cfg))]
2+
13
pub mod errors;
24
pub mod iter;
5+
pub mod format {
6+
mod json;
7+
#[cfg(feature = "serde")]
8+
pub use json::JsValueSerdeExt;
9+
}
310
use wasm_bindgen::UnwrapThrowExt;
411

512
/// Convenience function to avoid repeating expect logic.

crates/utils/tests/serde.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
function deepStrictEqual(left, right) {
2+
var left_json = JSON.stringify(left);
3+
var right_json = JSON.stringify(right);
4+
if (left_json !== right_json) {
5+
throw Error(`${left_json} != ${right_json}`)
6+
}
7+
}
8+
9+
export function verify_serde (a) {
10+
deepStrictEqual(a, {
11+
a: 0,
12+
b: 'foo',
13+
c: null,
14+
d: { a: 1 }
15+
});
16+
};
17+
18+
export function make_js_value() {
19+
return {
20+
a: 2,
21+
b: 'bar',
22+
c: { a: 3 },
23+
d: { a: 4 },
24+
}
25+
};

crates/utils/tests/serde.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#![cfg(target_arch = "wasm32")]
2+
#![cfg(feature = "serde")]
3+
extern crate wasm_bindgen;
4+
extern crate wasm_bindgen_test;
5+
6+
use wasm_bindgen::prelude::*;
7+
use wasm_bindgen_test::*;
8+
9+
use gloo_utils::format::JsValueSerdeExt;
10+
11+
use serde_derive::{Deserialize, Serialize};
12+
13+
wasm_bindgen_test_configure!(run_in_browser);
14+
15+
#[wasm_bindgen(start)]
16+
pub fn start() {
17+
panic!();
18+
}
19+
20+
#[wasm_bindgen(module = "/tests/serde.js")]
21+
extern "C" {
22+
fn verify_serde(val: JsValue);
23+
fn make_js_value() -> JsValue;
24+
}
25+
26+
#[derive(Deserialize, Serialize, Debug)]
27+
pub struct SerdeFoo {
28+
a: u32,
29+
b: String,
30+
c: Option<SerdeBar>,
31+
d: SerdeBar,
32+
}
33+
34+
#[derive(Deserialize, Serialize, Debug)]
35+
pub struct SerdeBar {
36+
a: u32,
37+
}
38+
39+
#[wasm_bindgen_test]
40+
fn from_serde() {
41+
let js = JsValue::from_serde("foo").unwrap();
42+
assert_eq!(js.as_string(), Some("foo".to_string()));
43+
44+
verify_serde(
45+
JsValue::from_serde(&SerdeFoo {
46+
a: 0,
47+
b: "foo".to_string(),
48+
c: None,
49+
d: SerdeBar { a: 1 },
50+
})
51+
.unwrap(),
52+
);
53+
}
54+
55+
#[wasm_bindgen_test]
56+
fn into_serde() {
57+
let js_value = make_js_value();
58+
let foo = js_value.into_serde::<SerdeFoo>().unwrap();
59+
assert_eq!(foo.a, 2);
60+
assert_eq!(foo.b, "bar");
61+
assert!(foo.c.is_some());
62+
assert_eq!(foo.c.as_ref().unwrap().a, 3);
63+
assert_eq!(foo.d.a, 4);
64+
65+
assert_eq!(JsValue::from("bar").into_serde::<String>().unwrap(), "bar");
66+
assert_eq!(JsValue::undefined().into_serde::<i32>().ok(), None);
67+
assert_eq!(JsValue::null().into_serde::<i32>().ok(), None);
68+
}

0 commit comments

Comments
 (0)