diff --git a/crates/console/Cargo.toml b/crates/console/Cargo.toml index ea3264bf..bf039794 100644 --- a/crates/console/Cargo.toml +++ b/crates/console/Cargo.toml @@ -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 = [ diff --git a/crates/console/src/lib.rs b/crates/console/src/lib.rs index d896013f..c62b0df5 100644 --- a/crates/console/src/lib.rs +++ b/crates/console/src/lib.rs @@ -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; @@ -38,7 +39,7 @@ pub mod __macro { data: impl serde::Serialize, columns: impl IntoIterator, ) { - let data = js_sys::JSON::parse(&serde_json::to_string(&data).unwrap_throw()).unwrap_throw(); + let data = ::from_serde(&data).unwrap_throw(); let columns = columns.into_iter().map(JsValue::from_str).collect(); crate::externs::table_with_data_and_columns(data, columns); diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml index 337e54d4..2c52efb7 100644 --- a/crates/net/Cargo.toml +++ b/crates/net/Cargo.toml @@ -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 } diff --git a/crates/net/src/http/mod.rs b/crates/net/src/http/mod.rs index 31524e0f..f357835a 100644 --- a/crates/net/src/http/mod.rs +++ b/crates/net/src/http/mod.rs @@ -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::{ @@ -391,16 +395,8 @@ impl Response { #[cfg_attr(docsrs, doc(cfg(feature = "json")))] pub async fn json(&self) -> Result { 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. diff --git a/crates/net/src/websocket/futures.rs b/crates/net/src/websocket/futures.rs index e42cf594..e2289de6 100644 --- a/crates/net/src/websocket/futures.rs +++ b/crates/net/src/websocket/futures.rs @@ -106,16 +106,13 @@ impl WebSocket { url: &str, protocols: &[S], ) -> Result { - 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 = ::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)) } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index db68154d..c5ef3a84 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -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" @@ -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"] diff --git a/crates/utils/src/format/json.rs b/crates/utils/src/format/json.rs new file mode 100644 index 00000000..549e90d2 --- /dev/null +++ b/crates/utils/src/format/json.rs @@ -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. +#[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::().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) -> serde_json::Result + 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(&self) -> serde_json::Result + where + T: for<'a> serde::de::Deserialize<'a>; +} + +impl JsValueSerdeExt for JsValue { + fn from_serde(t: &T) -> serde_json::Result + where + T: serde::ser::Serialize + ?Sized, + { + let s = serde_json::to_string(t)?; + Ok(js_sys::JSON::parse(&s).unwrap_throw()) + } + + fn into_serde(&self) -> serde_json::Result + 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) + } +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index f37ebb52..fedd87d8 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -1,5 +1,12 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] + pub mod errors; pub mod iter; +pub mod format { + mod json; + #[cfg(feature = "serde")] + pub use json::JsValueSerdeExt; +} use wasm_bindgen::UnwrapThrowExt; /// Convenience function to avoid repeating expect logic. diff --git a/crates/utils/tests/serde.js b/crates/utils/tests/serde.js new file mode 100644 index 00000000..ef0caf6e --- /dev/null +++ b/crates/utils/tests/serde.js @@ -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 }, + } +}; diff --git a/crates/utils/tests/serde.rs b/crates/utils/tests/serde.rs new file mode 100644 index 00000000..ee86e198 --- /dev/null +++ b/crates/utils/tests/serde.rs @@ -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, + 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::().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::().unwrap(), "bar"); + assert_eq!(JsValue::undefined().into_serde::().ok(), None); + assert_eq!(JsValue::null().into_serde::().ok(), None); +}