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

feat(gloo-utils): Lift serde-serialization from wasm-bindgen #242

Merged
merged 11 commits into from
Aug 21, 2022
Merged
2 changes: 1 addition & 1 deletion crates/console/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ categories = ["api-bindings", "development-tools::profiling", "wasm"]
wasm-bindgen = "0.2"
js-sys = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0"
gloo-utils = { version = "0.1", path = "../utils", features = ["serde"] }
[dependencies.web-sys]
version = "0.3"
features = [
Expand Down
3 changes: 2 additions & 1 deletion crates/console/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub use timer::Timer;

#[doc(hidden)]
pub mod __macro {
use gloo_utils::format::JsValueSerdeExt;
pub use js_sys::Array;
pub use wasm_bindgen::JsValue;
use wasm_bindgen::UnwrapThrowExt;
Expand All @@ -38,7 +39,7 @@ pub mod __macro {
data: impl serde::Serialize,
columns: impl IntoIterator<Item = &'a str>,
) {
let data = js_sys::JSON::parse(&serde_json::to_string(&data).unwrap_throw()).unwrap_throw();
let data = <JsValue as JsValueSerdeExt>::from_serde(&data).unwrap_throw();
let columns = columns.into_iter().map(JsValue::from_str).collect();

crate::externs::table_with_data_and_columns(data, columns);
Expand Down
2 changes: 1 addition & 1 deletion crates/net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ rustdoc-args = ["--cfg", "docsrs"]
wasm-bindgen = "0.2"
web-sys = "0.3"
js-sys = "0.3"
gloo-utils = { version = "0.1", path = "../utils" }
gloo-utils = { version = "0.1", path = "../utils", features = ["serde"] }

wasm-bindgen-futures = "0.4"
futures-core = { version = "0.3", optional = true }
Expand Down
16 changes: 6 additions & 10 deletions crates/net/src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ use wasm_bindgen_futures::JsFuture;
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
use serde::de::DeserializeOwned;

#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
use gloo_utils::format::JsValueSerdeExt;

pub use headers::Headers;
pub use query::QueryParams;
pub use web_sys::{
Expand Down Expand Up @@ -391,16 +395,8 @@ impl Response {
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub async fn json<T: DeserializeOwned>(&self) -> Result<T, Error> {
let promise = self.response.json().map_err(js_to_error)?;
let json = JsFuture::from(promise)
.await
.map_err(js_to_error)
.and_then(|json| {
js_sys::JSON::stringify(&json)
.map(String::from)
.map_err(js_to_error)
})?;

Ok(serde_json::from_str(&json)?)
let json = JsFuture::from(promise).await.map_err(js_to_error)?;
Ok(JsValueSerdeExt::into_serde(&json)?)
}

/// Reads the response as a String.
Expand Down
17 changes: 7 additions & 10 deletions crates/net/src/websocket/futures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,13 @@ impl WebSocket {
url: &str,
protocols: &[S],
) -> Result<Self, JsError> {
let map_err = |err| {
js_sys::Error::new(&format!(
"Failed to convert protocols to Javascript value: {}",
err
))
};
let json = serde_json::to_string(protocols)
.map_err(|e| map_err(e.to_string()))
.map(|d| js_sys::JSON::parse(&d).unwrap_throw())?;

let json = <JsValue as gloo_utils::format::JsValueSerdeExt>::from_serde(protocols)
.map_err(|err| {
js_sys::Error::new(&format!(
"Failed to convert protocols to Javascript value: {}",
err
))
})?;
Self::setup(web_sys::WebSocket::new_with_str_sequence(url, &json))
}

Expand Down
11 changes: 11 additions & 0 deletions crates/utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ categories = ["api-bindings", "wasm"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
serde = { version = "1.0", optional = true }
serde_json = {version = "1.0", optional = true }

[dependencies.web-sys]
version = "0.3"
Expand All @@ -27,5 +29,14 @@ features = [
"Element",
]

[features]
default = ["serde"]
serde = ["dep:serde", "dep:serde_json"]

[dev-dependencies]
wasm-bindgen-test = "0.3"
serde_derive = "1.0"

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
111 changes: 111 additions & 0 deletions crates/utils/src/format/json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#![cfg(feature = "serde")]

use wasm_bindgen::{JsValue, UnwrapThrowExt};
mod private {
pub trait Sealed {}
impl Sealed for wasm_bindgen::JsValue {}
}

/// Extenstion trait to provide conversion between [`JsValue`](wasm_bindgen::JsValue) and [`serde`].
///
/// Usage of this API requires activating the `serde` feature of the `gloo-utils` crate.
ranile marked this conversation as resolved.
Show resolved Hide resolved
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
pub trait JsValueSerdeExt: private::Sealed {
/// Creates a new `JsValue` from the JSON serialization of the object `t`
/// provided.
///
/// This function will serialize the provided value `t` to a JSON string,
/// send the JSON string to JS, parse it into a JS object, and then return
/// a handle to the JS object. This is unlikely to be super speedy so it's
/// not recommended for large payloads, but it's a nice to have in some
/// situations!
///
/// Usage of this API requires activating the `serde` feature of
/// the `gloo-utils` crate.
/// # Example
///
/// ```rust
/// use wasm_bindgen::JsValue;
/// use gloo_utils::format::JsValueSerdeExt;
///
/// # fn no_run() {
/// assert_eq!(JsValue::from("bar").into_serde::<String>().unwrap(), "bar");
/// # }
/// ```
/// # Errors
///
/// Returns any error encountered when serializing `T` into JSON.
///
/// # Panics
///
/// Panics if [`serde_json`](serde_json::to_string) generated JSON that couldn't be parsed by [`js_sys`].
/// Uses [`unwrap_throw`](UnwrapThrowExt::unwrap_throw) from [`wasm_bindgen::UnwrapThrowExt`].
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
fn from_serde<T>(t: &T) -> serde_json::Result<JsValue>
where
T: serde::ser::Serialize + ?Sized;

/// Invokes `JSON.stringify` on this value and then parses the resulting
/// JSON into an arbitrary Rust value.
///
/// This function will first call `JSON.stringify` on the `JsValue` itself.
/// The resulting string is then passed into Rust which then parses it as
/// JSON into the resulting value. If given `undefined`, object will be silentrly changed to
/// null to avoid panic.
///
/// Usage of this API requires activating the `serde` feature of
/// the `gloo-utils` crate.
///
/// # Example
///
/// ```rust
/// use wasm_bindgen::JsValue;
/// use gloo_utils::format::JsValueSerdeExt;
///
/// # fn no_run() {
/// let array = vec![1,2,3];
/// let obj = JsValue::from_serde(&array);
/// # }
/// ```
///
/// # Errors
///
/// Returns any error encountered when parsing the JSON into a `T`.
///
/// # Panics
///
/// Panics if [`js_sys`] couldn't stringify the JsValue. Uses [`unwrap_throw`](UnwrapThrowExt::unwrap_throw)
/// from [`wasm_bindgen::UnwrapThrowExt`].
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
#[allow(clippy::wrong_self_convention)]
fn into_serde<T>(&self) -> serde_json::Result<T>
where
T: for<'a> serde::de::Deserialize<'a>;
}

impl JsValueSerdeExt for JsValue {
fn from_serde<T>(t: &T) -> serde_json::Result<JsValue>
where
T: serde::ser::Serialize + ?Sized,
{
let s = serde_json::to_string(t)?;
Ok(js_sys::JSON::parse(&s).unwrap_throw())
}

fn into_serde<T>(&self) -> serde_json::Result<T>
where
T: for<'a> serde::de::Deserialize<'a>,
{
// Turns out `JSON.stringify(undefined) === undefined`, so if
// we're passed `undefined` reinterpret it as `null` for JSON
// purposes.
let s = if self.is_undefined() {
String::from("null")
} else {
js_sys::JSON::stringify(self)
.map(String::from)
.unwrap_throw()
};
serde_json::from_str(&s)
}
}
7 changes: 7 additions & 0 deletions crates/utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
#![cfg_attr(docsrs, feature(doc_cfg))]

pub mod errors;
pub mod iter;
pub mod format {
mod json;
andoriyu marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(feature = "serde")]
pub use json::JsValueSerdeExt;
}
use wasm_bindgen::UnwrapThrowExt;

/// Convenience function to avoid repeating expect logic.
Expand Down
25 changes: 25 additions & 0 deletions crates/utils/tests/serde.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
function deepStrictEqual(left, right) {
var left_json = JSON.stringify(left);
var right_json = JSON.stringify(right);
if (left_json !== right_json) {
throw Error(`${left_json} != ${right_json}`)
}
}

export function verify_serde (a) {
deepStrictEqual(a, {
a: 0,
b: 'foo',
c: null,
d: { a: 1 }
});
};

export function make_js_value() {
return {
a: 2,
b: 'bar',
c: { a: 3 },
d: { a: 4 },
}
};
68 changes: 68 additions & 0 deletions crates/utils/tests/serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#![cfg(target_arch = "wasm32")]
#![cfg(feature = "serde")]
extern crate wasm_bindgen;
extern crate wasm_bindgen_test;

use wasm_bindgen::prelude::*;
use wasm_bindgen_test::*;

use gloo_utils::format::JsValueSerdeExt;

use serde_derive::{Deserialize, Serialize};

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen(start)]
pub fn start() {
panic!();
}

#[wasm_bindgen(module = "/tests/serde.js")]
extern "C" {
fn verify_serde(val: JsValue);
fn make_js_value() -> JsValue;
}

#[derive(Deserialize, Serialize, Debug)]
pub struct SerdeFoo {
a: u32,
b: String,
c: Option<SerdeBar>,
d: SerdeBar,
}

#[derive(Deserialize, Serialize, Debug)]
pub struct SerdeBar {
a: u32,
}

#[wasm_bindgen_test]
fn from_serde() {
let js = JsValue::from_serde("foo").unwrap();
assert_eq!(js.as_string(), Some("foo".to_string()));

verify_serde(
JsValue::from_serde(&SerdeFoo {
a: 0,
b: "foo".to_string(),
c: None,
d: SerdeBar { a: 1 },
})
.unwrap(),
);
}

#[wasm_bindgen_test]
fn into_serde() {
let js_value = make_js_value();
let foo = js_value.into_serde::<SerdeFoo>().unwrap();
assert_eq!(foo.a, 2);
assert_eq!(foo.b, "bar");
assert!(foo.c.is_some());
assert_eq!(foo.c.as_ref().unwrap().a, 3);
assert_eq!(foo.d.a, 4);

assert_eq!(JsValue::from("bar").into_serde::<String>().unwrap(), "bar");
assert_eq!(JsValue::undefined().into_serde::<i32>().ok(), None);
assert_eq!(JsValue::null().into_serde::<i32>().ok(), None);
}