diff --git a/crates/net/src/eventsource/futures.rs b/crates/net/src/eventsource/futures.rs index 4d78eba6..5d274128 100644 --- a/crates/net/src/eventsource/futures.rs +++ b/crates/net/src/eventsource/futures.rs @@ -50,7 +50,7 @@ use web_sys::MessageEvent; /// Wrapper around browser's EventSource API. Dropping /// this will close the underlying event source. -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub struct EventSource { es: web_sys::EventSource, } @@ -178,13 +178,7 @@ impl EventSource { /// The current state of the EventSource. pub fn state(&self) -> State { - let ready_state = self.es.ready_state(); - match ready_state { - 0 => State::Connecting, - 1 => State::Open, - 2 => State::Closed, - _ => unreachable!(), - } + self.es.ready_state().into() } } diff --git a/crates/net/src/eventsource/mod.rs b/crates/net/src/eventsource/mod.rs index ee28d883..df55d96f 100644 --- a/crates/net/src/eventsource/mod.rs +++ b/crates/net/src/eventsource/mod.rs @@ -11,7 +11,8 @@ use std::fmt; /// /// See [`EventSource.readyState` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/readyState) /// to learn more. -#[derive(Copy, Clone, Debug)] +// This trait implements `Ord`, use caution when changing the order of the variants. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum State { /// The connection has not yet been established. Connecting, @@ -21,8 +22,55 @@ pub enum State { Closed, } +impl State { + /// Returns the state as a &'static str. + /// + /// # Example + /// + /// ``` + /// # use gloo_net::eventsource::State; + /// assert_eq!(State::Connecting.as_str(), "connecting"); + /// assert_eq!(State::Open.as_str(), "open"); + /// assert_eq!(State::Closed.as_str(), "closed"); + /// ``` + pub const fn as_str(&self) -> &'static str { + match self { + State::Connecting => "connecting", + State::Open => "open", + State::Closed => "closed", + } + } +} + +impl fmt::Display for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl From for State { + fn from(state: u16) -> Self { + match state { + 0 => State::Connecting, + 1 => State::Open, + 2 => State::Closed, + _ => unreachable!("Invalid readyState"), + } + } +} + +impl From for u16 { + fn from(state: State) -> Self { + match state { + State::Connecting => 0, + State::Open => 1, + State::Closed => 2, + } + } +} + /// Error returned by the EventSource -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] #[non_exhaustive] #[allow(missing_copy_implementations)] pub enum EventSourceError { @@ -37,3 +85,21 @@ impl fmt::Display for EventSourceError { } } } + +#[cfg(test)] +mod tests { + use crate::is_strictly_sorted; + + use super::*; + + #[test] + fn test_order() { + let expected_order = vec![State::Connecting, State::Open, State::Closed]; + + assert!(is_strictly_sorted(&expected_order)); + + // Check that the u16 conversion is also sorted + let order: Vec<_> = expected_order.iter().map(|s| u16::from(*s)).collect(); + assert!(is_strictly_sorted(&order)); + } +} diff --git a/crates/net/src/http/headers.rs b/crates/net/src/http/headers.rs index 0a9b1515..3b3a3d39 100644 --- a/crates/net/src/http/headers.rs +++ b/crates/net/src/http/headers.rs @@ -1,21 +1,13 @@ use gloo_utils::iter::UncheckedIter; use js_sys::{Array, Map}; -use std::fmt; +use std::{fmt, iter::FromIterator}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; -// I experimented with using `js_sys::Object` for the headers, since this object is marked -// experimental in MDN. However it's in the fetch spec, and it's necessary for appending headers. /// A wrapper around `web_sys::Headers`. pub struct Headers { raw: web_sys::Headers, } -impl Default for Headers { - fn default() -> Self { - Self::new() - } -} - impl Headers { /// Create a new empty headers object. pub fn new() -> Self { @@ -37,19 +29,33 @@ impl Headers { /// This method appends a new value onto an existing header, or adds the header if it does not /// already exist. - pub fn append(&self, name: &str, value: &str) { + /// + /// # Examples + /// + /// ``` + /// # use gloo_net::http::Headers; + /// # fn no_run() { + /// let headers = Headers::new(); + /// headers.append("Content-Type", "text/plain"); + /// assert_eq!(headers.get("Content-Type"), Some("text/plain".to_string())); + /// + /// headers.append("Content-Type", "text/html"); + /// assert_eq!(headers.get("Content-Type"), Some("text/plain, text/html".to_string())); + /// # } + /// ``` + pub fn append(&mut self, name: &str, value: &str) { // XXX Can this throw? WEBIDL says yes, my experiments with forbidden headers and MDN say // no. self.raw.append(name, value).unwrap_throw() } /// Deletes a header if it is present. - pub fn delete(&self, name: &str) { + pub fn delete(&mut self, name: &str) { self.raw.delete(name).unwrap_throw() } /// Gets a header if it is present. - pub fn get(&self, name: &str) -> Option { + pub fn get(&mut self, name: &str) -> Option { self.raw.get(name).unwrap_throw() } @@ -59,7 +65,7 @@ impl Headers { } /// Overwrites a header with the given name. - pub fn set(&self, name: &str, value: &str) { + pub fn set(&mut self, name: &str, value: &str) { self.raw.set(name, value).unwrap_throw() } @@ -93,6 +99,42 @@ impl Headers { } } +impl Clone for Headers { + fn clone(&self) -> Self { + self.entries().collect() + } +} + +impl Default for Headers { + fn default() -> Self { + Self::new() + } +} + +impl Extend<(K, V)> for Headers +where + K: AsRef, + V: AsRef, +{ + fn extend>(&mut self, iter: T) { + for (key, value) in iter { + self.append(key.as_ref(), value.as_ref()); + } + } +} + +impl FromIterator<(K, V)> for Headers +where + K: AsRef, + V: AsRef, +{ + fn from_iter>(iter: T) -> Self { + let mut headers = Self::new(); + headers.extend(iter); + headers + } +} + impl fmt::Debug for Headers { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut dbg = f.debug_struct("Headers"); diff --git a/crates/net/src/http/query.rs b/crates/net/src/http/query.rs index 50a9e5d6..b6ebbc0b 100644 --- a/crates/net/src/http/query.rs +++ b/crates/net/src/http/query.rs @@ -1,6 +1,6 @@ use gloo_utils::iter::UncheckedIter; use js_sys::{Array, Map}; -use std::fmt; +use std::{fmt, iter::FromIterator}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; /// A sequence of URL query parameters, wrapping [`web_sys::UrlSearchParams`]. @@ -29,8 +29,13 @@ impl QueryParams { Self { raw } } + /// Create [`web_sys::UrlSearchParams`] from [`QueryParams`] object. + pub fn into_raw(self) -> web_sys::UrlSearchParams { + self.raw + } + /// Append a parameter to the query string. - pub fn append(&self, name: &str, value: &str) { + pub fn append(&mut self, name: &str, value: &str) { self.raw.append(name, value) } @@ -50,7 +55,7 @@ impl QueryParams { } /// Remove all occurrences of a parameter from the query string. - pub fn delete(&self, name: &str) { + pub fn delete(&mut self, name: &str) { self.raw.delete(name) } @@ -72,6 +77,36 @@ impl QueryParams { } } +impl Clone for QueryParams { + fn clone(&self) -> Self { + self.iter().collect() + } +} + +impl Extend<(K, V)> for QueryParams +where + K: AsRef, + V: AsRef, +{ + fn extend>(&mut self, iter: I) { + for (key, value) in iter { + self.append(key.as_ref(), value.as_ref()); + } + } +} + +impl FromIterator<(K, V)> for QueryParams +where + K: AsRef, + V: AsRef, +{ + fn from_iter>(iter: I) -> Self { + let mut params = Self::new(); + params.extend(iter); + params + } +} + /// The formatted query parameters ready to be used in a URL query string. /// /// # Examples diff --git a/crates/net/src/http/request.rs b/crates/net/src/http/request.rs index 537fc264..dea1153b 100644 --- a/crates/net/src/http/request.rs +++ b/crates/net/src/http/request.rs @@ -16,7 +16,7 @@ use web_sys::{ #[cfg_attr(docsrs, doc(cfg(feature = "json")))] use serde::de::DeserializeOwned; -/// A wrapper round `web_sys::Request`: an http request to be used with the `fetch` API. +/// A builder for [`Request`]. pub struct RequestBuilder { options: web_sys::RequestInit, headers: Headers, @@ -38,10 +38,9 @@ impl RequestBuilder { } /// Set the body for this request. - pub fn body(mut self, body: impl Into) -> Result { + pub fn body(mut self, body: impl Into) -> Self { self.options.body(Some(&body.into())); - - self.try_into() + self } /// A string indicating how the request will interact with the browser’s HTTP cache. @@ -64,7 +63,7 @@ impl RequestBuilder { } /// Sets a header. - pub fn header(self, key: &str, value: &str) -> Self { + pub fn header(mut self, key: &str, value: &str) -> Self { self.headers.set(key, value); self } @@ -96,14 +95,13 @@ impl RequestBuilder { /// // Result URL: /search?key=value&a=3&b=4&key=another_value /// # } /// ``` - pub fn query<'a, T, V>(self, params: T) -> Self + pub fn query(mut self, params: T) -> Self where - T: IntoIterator, + T: IntoIterator, + K: AsRef, V: AsRef, { - for (name, value) in params { - self.query.append(name, value.as_ref()); - } + self.query.extend(params); self } @@ -114,16 +112,20 @@ impl RequestBuilder { self } - /// A convenience method to set JSON as request body + /// A convenience method to set JSON as request body. /// /// # Note /// /// This method also sets the `Content-Type` header to `application/json` + /// + /// # Errors + /// + /// This method will return an error if the value cannot be serialized #[cfg(feature = "json")] #[cfg_attr(docsrs, doc(cfg(feature = "json")))] - pub fn json(self, value: &T) -> Result { + pub fn json(self, value: &T) -> Result { let json = serde_json::to_string(value)?; - self.header("Content-Type", "application/json").body(json) + Ok(self.header("Content-Type", "application/json").body(json)) } /// The request method, e.g., GET, POST. @@ -177,19 +179,21 @@ impl RequestBuilder { self.options.signal(signal); self } + /// Builds the request and send it to the server, returning the received response. pub async fn send(self) -> Result { let req: Request = self.try_into()?; req.send().await } + /// Builds the request. - pub fn build(self) -> Result { + pub fn build(self) -> Result { self.try_into() } } impl TryFrom for Request { - type Error = crate::error::Error; + type Error = crate::Error; fn try_from(mut value: RequestBuilder) -> Result { // To preserve existing query parameters of self.url, it must be parsed and extended with @@ -208,7 +212,7 @@ impl TryFrom for Request { let request = web_sys::Request::new_with_str_and_init(&final_url, &value.options) .map_err(js_to_error)?; - Ok(request.into()) + Ok(Request::from_raw(request)) } } @@ -222,27 +226,41 @@ impl fmt::Debug for RequestBuilder { pub struct Request(web_sys::Request); impl Request { - /// Creates a new [`GET`][Method::GET] `Request` with url. + /// Creates a new [`Request`] from a [`web_sys::Request`]. + /// + /// # Note + /// + /// If the body of the request has already been read, other body readers will misbehave. + pub fn from_raw(request: web_sys::Request) -> Self { + Self(request) + } + + /// Returns the underlying [`web_sys::Request`]. + pub fn into_raw(self) -> web_sys::Request { + self.0 + } + + /// Creates a new GET [`RequestBuilder`] with url. pub fn get(url: &str) -> RequestBuilder { RequestBuilder::new(url).method(Method::GET) } - /// Creates a new [`POST`][Method::POST] `Request` with url. + /// Creates a new POST [`RequestBuilder`] with url. pub fn post(url: &str) -> RequestBuilder { RequestBuilder::new(url).method(Method::POST) } - /// Creates a new [`PUT`][Method::PUT] `Request` with url. + /// Creates a new PUT [`RequestBuilder`] with url. pub fn put(url: &str) -> RequestBuilder { RequestBuilder::new(url).method(Method::PUT) } - /// Creates a new [`DELETE`][Method::DELETE] `Request` with url. + /// Creates a new Delete [`RequestBuilder`] with url. pub fn delete(url: &str) -> RequestBuilder { RequestBuilder::new(url).method(Method::DELETE) } - /// Creates a new [`PATCH`][Method::PATCH] `Request` with url. + /// Creates a new PATCH [`RequestBuilder`] with url. pub fn patch(url: &str) -> RequestBuilder { RequestBuilder::new(url).method(Method::PATCH) } @@ -257,49 +275,110 @@ impl Request { Headers::from_raw(self.0.headers()) } + /// Return the read only mode for the request + pub fn mode(&self) -> RequestMode { + self.0.mode() + } + + /// Return the parsed method for the request + pub fn method(&self) -> Method { + Method::from_str(self.0.method().as_str()).unwrap() + } + /// Has the request body been consumed? /// - /// If true, then any future attempts to consume the body will error. + /// If true, then any future attempts to consume the body will panic. + /// + /// # Note + /// + /// In normal usage, this should always return false. The body is only consumed + /// by methods that take ownership of the request. However, if you manually + /// build a [`Request`] from [`web_sys::Request`], then this could be true. pub fn body_used(&self) -> bool { self.0.body_used() } - /// Gets the body. - pub fn body(&self) -> Option { + /// Returns the underlying body of the request. + /// + /// # Note + /// + /// This consumes the request, if you need to access the body multiple times, + /// you should `clone` the request first. + pub fn body(self) -> Option { self.0.body() } - /// Reads the request to completion, returning it as `FormData`. - pub async fn form_data(&self) -> Result { + /// Returns the underlying body of the request as [`web_sys::FormData`]. + /// + /// # Note + /// + /// This consumes the request, if you need to access the body multiple times, + /// you should `clone` the request first. + /// + /// # Errors + /// + /// Throws a "TypeError" if the content type of the request is not `"multipart/form-data"` or + /// `"application/x-www-form-urlencoded"`. + /// + /// Throws a "TypeError" if the body cannot be converted to [`web_sys::FormData`]. + pub async fn form_data(self) -> Result { let promise = self.0.form_data().map_err(js_to_error)?; - let val = JsFuture::from(promise).await.map_err(js_to_error)?; + let val = JsFuture::from(promise).await.map_err(js_to_error)?; // should never fail? Ok(FormData::from(val)) } - /// Reads the request to completion, parsing it as JSON. + /// Returns the underlying body as a string. + /// + /// # Note + /// + /// This consumes the request, if you need to access the body multiple times, + /// you should `clone` the request first. + /// + /// # Errors + /// + /// This will return an error if the body cannot be decoded as utf-8. + pub async fn text(self) -> Result { + let promise = self.0.text().map_err(js_to_error)?; + let val = JsFuture::from(promise).await.map_err(js_to_error)?; // should never fail? + let string = js_sys::JsString::from(val); + Ok(String::from(&string)) + } + + /// Returns the underlying body of the request and parses it as JSON. + /// + /// # Note + /// + /// This consumes the request, if you need to access the body multiple times, + /// you should `clone` the request first. + /// + /// # Errors + /// + /// This will return an error if the body text cannot be decoded as utf-8 or + /// if the JSON cannot be deserialized. #[cfg(feature = "json")] #[cfg_attr(docsrs, doc(cfg(feature = "json")))] - pub async fn json(&self) -> Result { + pub async fn json(self) -> Result { serde_json::from_str::(&self.text().await?).map_err(Error::from) } - /// Reads the reqeust as a String. - pub async fn text(&self) -> Result { - let promise = self.0.text().unwrap(); - let val = JsFuture::from(promise).await.map_err(js_to_error)?; - let string = js_sys::JsString::from(val); - Ok(String::from(&string)) - } - /// Gets the binary request /// /// This works by obtaining the response as an `ArrayBuffer`, creating a `Uint8Array` from it /// and then converting it to `Vec` - pub async fn binary(&self) -> Result, Error> { - let promise = self.0.array_buffer().map_err(js_to_error)?; + /// + /// # Note + /// + /// This consumes the request, if you need to access the body multiple times, + /// you should `clone` the request first. + /// + /// # Errors + /// + /// This method may return a "RangeError" + pub async fn binary(self) -> Result, Error> { + let promise = self.0.array_buffer().map_err(js_to_error)?; // RangeError let array_buffer: ArrayBuffer = JsFuture::from(promise) .await - .map_err(js_to_error)? + .map_err(js_to_error)? // should never fail? .unchecked_into(); let typed_buff: Uint8Array = Uint8Array::new(&array_buffer); let mut body = vec![0; typed_buff.length() as usize]; @@ -307,17 +386,7 @@ impl Request { Ok(body) } - /// Return the read only mode for the request - pub fn mode(&self) -> RequestMode { - self.0.mode() - } - - /// Return the parsed method for the request - pub fn method(&self) -> Method { - Method::from_str(self.0.method().as_str()).unwrap() - } - - /// Executes the request. + /// Executes the request, using the `fetch` API. pub async fn send(self) -> Result { let request = self.0; let global = js_sys::global(); @@ -341,19 +410,16 @@ impl Request { response .dyn_into::() .map_err(|e| panic!("fetch returned {:?}, not `Response` - this is a bug", e)) - .map(Response::from) - } -} - -impl From for Request { - fn from(raw: web_sys::Request) -> Self { - Request(raw) + .map(Response::from_raw) } } -impl From for web_sys::Request { - fn from(val: Request) -> Self { - val.0 +impl Clone for Request { + fn clone(&self) -> Self { + // Cloning the underlying request could fail if the request body has already been consumed. + // This should not happen in normal usage since the body is consumed only when `send` is called. + debug_assert!(!self.body_used()); + Self(self.0.clone().unwrap()) } } diff --git a/crates/net/src/http/response.rs b/crates/net/src/http/response.rs index 3f1e6f94..460ef2aa 100644 --- a/crates/net/src/http/response.rs +++ b/crates/net/src/http/response.rs @@ -19,6 +19,21 @@ impl Response { pub fn builder() -> ResponseBuilder { ResponseBuilder::new() } + + /// Creates a new [`Response`] from a [`web_sys::Request`]. + /// + /// # Note + /// + /// If the body of the response has already been read, other body readers will misbehave. + pub fn from_raw(request: web_sys::Response) -> Self { + Self(request) + } + + /// Returns the underlying [`web_sys::Response`]. + pub fn into_raw(self) -> web_sys::Response { + self.0 + } + /// The type read-only property of the Response interface contains the type of the response. /// /// It can be one of the following: @@ -78,43 +93,93 @@ impl Response { /// Has the response body been consumed? /// - /// If true, then any future attempts to consume the body will error. + /// If true, then any future attempts to consume the body will panic. + /// + /// # Note + /// + /// In normal usage, this should always return false. The body is only consumed + /// by methods that take ownership of the response. However, if you manually + /// build a [`Response`] from [`web_sys::Response`], then this could be true. pub fn body_used(&self) -> bool { self.0.body_used() } - /// Gets the body. - pub fn body(&self) -> Option { + /// Returns the underlying body of the response. + /// + /// # Note + /// + /// This consumes the response, if you need to access the body multiple times, + /// you should `clone` the response first. + pub fn body(self) -> Option { self.0.body() } - /// Reads the response to completion, returning it as `FormData`. - pub async fn form_data(&self) -> Result { + /// Returns the underlying body as [`web_sys::FormData`]. + /// + /// # Note + /// + /// This consumes the request, if you need to access the body multiple times, + /// you should `clone` the request first. + /// + /// # Errors + /// + /// Throws a "TypeError" if the content type of the request is not `"multipart/form-data"` or + /// `"application/x-www-form-urlencoded"`. + /// + /// Throws a "TypeError" if the body cannot be converted to [`web_sys::FormData`]. + pub async fn form_data(self) -> Result { let promise = self.0.form_data().map_err(js_to_error)?; let val = JsFuture::from(promise).await.map_err(js_to_error)?; Ok(web_sys::FormData::from(val)) } - /// Reads the response to completion, parsing it as JSON. - #[cfg(feature = "json")] - #[cfg_attr(docsrs, doc(cfg(feature = "json")))] - pub async fn json(&self) -> Result { - serde_json::from_str::(&self.text().await?).map_err(Error::from) - } - - /// Reads the response as a String. - pub async fn text(&self) -> Result { + /// Returns the underlying body as a string. + /// + /// # Note + /// + /// This consumes the response, if you need to access the body multiple times, + /// you should `clone` the response first. + /// + /// # Errors + /// + /// This will return an error if the body cannot be decoded as utf-8. + pub async fn text(self) -> Result { let promise = self.0.text().unwrap(); let val = JsFuture::from(promise).await.map_err(js_to_error)?; let string = js_sys::JsString::from(val); Ok(String::from(&string)) } + /// Returns the underlying body as a string. + /// + /// # Note + /// + /// This consumes the response, if you need to access the body multiple times, + /// you should `clone` the response first. + /// + /// # Errors + /// + /// This will return an error if the body cannot be decoded as utf-8. + #[cfg(feature = "json")] + #[cfg_attr(docsrs, doc(cfg(feature = "json")))] + pub async fn json(self) -> Result { + serde_json::from_str::(&self.text().await?).map_err(Error::from) + } + /// Gets the binary response /// /// This works by obtaining the response as an `ArrayBuffer`, creating a `Uint8Array` from it /// and then converting it to `Vec` - pub async fn binary(&self) -> Result, Error> { + /// + /// # Note + /// + /// This consumes the response, if you need to access the body multiple times, + /// you should `clone` the response first. + /// + /// # Errors + /// + /// This method may return a "RangeError" + pub async fn binary(self) -> Result, Error> { let promise = self.0.array_buffer().map_err(js_to_error)?; let array_buffer: ArrayBuffer = JsFuture::from(promise) .await @@ -127,15 +192,13 @@ impl Response { } } -impl From for Response { - fn from(raw: web_sys::Response) -> Self { - Self(raw) - } -} - -impl From for web_sys::Response { - fn from(res: Response) -> Self { - res.0 +impl Clone for Response { + fn clone(&self) -> Self { + // Cloning the underlying response could fail if the response body has already been consumed, + // which should not happen in normal usage since the body is consumed only by methods that + // take ownership of the response. + debug_assert!(self.body_used()); + Self(self.0.clone().unwrap()) } } @@ -165,14 +228,14 @@ impl ResponseBuilder { Self::default() } - /// Replace _all_ the headers. + /// Replaces _all_ the headers. pub fn headers(mut self, headers: Headers) -> Self { self.headers = headers; self } /// Sets a header. - pub fn header(self, key: &str, value: &str) -> Self { + pub fn header(mut self, key: &str, value: &str) -> Self { self.headers.set(key, value); self } @@ -194,6 +257,10 @@ impl ResponseBuilder { /// # Note /// /// This method also sets the `Content-Type` header to `application/json` + /// + /// # Errors + /// + /// This method will return an error if the value cannot be serialized to JSON #[cfg(feature = "json")] #[cfg_attr(docsrs, doc(cfg(feature = "json")))] pub fn json(self, value: &T) -> Result { @@ -203,6 +270,11 @@ impl ResponseBuilder { } /// Set the response body and return the response + /// + /// # Errors + /// + /// This method will return an error if the response cannot be created + // TODO: Can this method actually fail in normal usage? pub fn body(mut self, data: T) -> Result where T: IntoRawResponse, diff --git a/crates/net/src/lib.rs b/crates/net/src/lib.rs index 0a9cf638..47c234ec 100644 --- a/crates/net/src/lib.rs +++ b/crates/net/src/lib.rs @@ -22,3 +22,13 @@ pub mod http; pub mod websocket; pub use error::*; + +#[cfg(test)] +/// Checks if a slice is strictly sorted. +/// +/// Strictly sorted means that each element is _less_ than the next. +/// +/// TODO: Use `is_sorted` when it becomes stable. +fn is_strictly_sorted(slice: &[T]) -> bool { + slice.iter().zip(slice.iter().skip(1)).all(|(a, b)| a < b) +} diff --git a/crates/net/src/websocket/events.rs b/crates/net/src/websocket/events.rs index 5261ab76..c21f8da9 100644 --- a/crates/net/src/websocket/events.rs +++ b/crates/net/src/websocket/events.rs @@ -1,7 +1,7 @@ //! WebSocket Events /// Data emitted by `onclose` event -#[derive(Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct CloseEvent { /// Close code pub code: u16, diff --git a/crates/net/src/websocket/futures.rs b/crates/net/src/websocket/futures.rs index ef8de17b..c00bd43b 100644 --- a/crates/net/src/websocket/futures.rs +++ b/crates/net/src/websocket/futures.rs @@ -217,14 +217,7 @@ impl WebSocket { /// The current state of the websocket. pub fn state(&self) -> State { - let ready_state = self.ws.ready_state(); - match ready_state { - 0 => State::Connecting, - 1 => State::Open, - 2 => State::Closing, - 3 => State::Closed, - _ => unreachable!(), - } + self.ws.ready_state().into() } /// The extensions in use. diff --git a/crates/net/src/websocket/mod.rs b/crates/net/src/websocket/mod.rs index 188a7b59..b3343b46 100644 --- a/crates/net/src/websocket/mod.rs +++ b/crates/net/src/websocket/mod.rs @@ -11,7 +11,7 @@ use gloo_utils::errors::JsError; use std::fmt; /// Message sent to and received from WebSocket. -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Hash)] pub enum Message { /// String message Text(String), @@ -23,7 +23,8 @@ pub enum Message { /// /// See [`WebSocket.readyState` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) /// to learn more. -#[derive(Copy, Clone, Debug)] +// This trait implements `Ord`, use caution when changing the order of the variants. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum State { /// The connection has not yet been established. Connecting, @@ -36,6 +37,29 @@ pub enum State { Closed, } +impl From for State { + fn from(state: u16) -> Self { + match state { + 0 => State::Connecting, + 1 => State::Open, + 2 => State::Closing, + 3 => State::Closed, + _ => unreachable!("invalid state"), + } + } +} + +impl From for u16 { + fn from(state: State) -> Self { + match state { + State::Connecting => 0, + State::Open => 1, + State::Closing => 2, + State::Closed => 3, + } + } +} + /// Error returned by WebSocket #[derive(Debug)] #[non_exhaustive] @@ -63,3 +87,26 @@ impl fmt::Display for WebSocketError { } impl std::error::Error for WebSocketError {} + +#[cfg(test)] +mod tests { + use crate::is_strictly_sorted; + + use super::*; + + #[test] + fn test_order() { + let expected_order = vec![ + State::Connecting, + State::Open, + State::Closing, + State::Closed, + ]; + + assert!(is_strictly_sorted(&expected_order)); + + // Check that the u16 conversion is also sorted + let order: Vec<_> = expected_order.iter().map(|s| u16::from(*s)).collect(); + assert!(is_strictly_sorted(&order)); + } +} diff --git a/crates/net/tests/headers.rs b/crates/net/tests/headers.rs new file mode 100644 index 00000000..90acc019 --- /dev/null +++ b/crates/net/tests/headers.rs @@ -0,0 +1,57 @@ +use std::iter::FromIterator; + +use gloo_net::http::Headers; +use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn headers_append() { + let mut headers = Headers::new(); + headers.append("Content-Type", "text/plain"); + assert_eq!(headers.get("Content-Type"), Some("text/plain".to_string())); + + headers.append("Content-Type", "text/html"); + assert_eq!( + headers.get("Content-Type"), + Some("text/plain, text/html".to_string()) + ); +} + +#[wasm_bindgen_test] +fn headers_from_iter() { + // Test from_iter + let mut headers = Headers::from_iter(vec![ + ("Content-Type", "text/plain"), + ("Content-Type", "text/html"), + ("X-Test", "test"), + ]); + assert_eq!( + headers.get("Content-Type"), + Some("text/plain, text/html".to_string()) + ); + assert_eq!(headers.get("X-Test"), Some("test".to_string())); + + // Test extend + headers.extend(vec![("X-Test", "test2")]); + assert_eq!(headers.get("X-Test"), Some("test, test2".to_string())); +} + +#[wasm_bindgen_test] +fn headers_clone() { + // Verify that a deep copy is made + let mut headers1 = Headers::new(); + headers1.set("Content-Type", "text/plain"); + + let mut headers2 = headers1.clone(); + assert_eq!(headers1.get("Content-Type"), Some("text/plain".to_string())); + assert_eq!(headers2.get("Content-Type"), Some("text/plain".to_string())); + + headers1.set("Content-Type", "text/html"); + assert_eq!(headers1.get("Content-Type"), Some("text/html".to_string())); + assert_eq!(headers2.get("Content-Type"), Some("text/plain".to_string())); + + headers2.set("Content-Type", "text/css"); + assert_eq!(headers1.get("Content-Type"), Some("text/html".to_string())); + assert_eq!(headers2.get("Content-Type"), Some("text/css".to_string())); +} diff --git a/crates/net/tests/http.rs b/crates/net/tests/http.rs index 7f956a6b..3d5fbd57 100644 --- a/crates/net/tests/http.rs +++ b/crates/net/tests/http.rs @@ -26,8 +26,8 @@ async fn fetch_json() { let url = format!("{}/get", *HTTPBIN_URL); let resp = Request::get(&url).send().await.unwrap(); - let json: HttpBin = resp.json().await.unwrap(); assert_eq!(resp.status(), 200); + let json: HttpBin = resp.json().await.unwrap(); assert_eq!(json.url, url); } @@ -53,8 +53,9 @@ async fn gzip_response() { .send() .await .unwrap(); - let json: HttpBin = resp.json().await.unwrap(); + assert_eq!(resp.status(), 200); + let json: HttpBin = resp.json().await.unwrap(); assert!(json.gzipped); } @@ -95,8 +96,9 @@ async fn post_json() { .send() .await .unwrap(); - let resp: HttpBin = req.json().await.unwrap(); + assert_eq!(req.status(), 200); + let resp: HttpBin = req.json().await.unwrap(); assert_eq!(resp.json.data, "data"); assert_eq!(resp.json.num, 42); } @@ -112,8 +114,9 @@ async fn fetch_binary() { .send() .await .unwrap(); - let json = resp.binary().await.unwrap(); + assert_eq!(resp.status(), 200); + let json = resp.binary().await.unwrap(); let json: HttpBin = serde_json::from_slice(&json).unwrap(); assert_eq!(json.data, ""); // default is empty string } @@ -137,3 +140,45 @@ async fn query_preserve_duplicate_params() { .unwrap(); assert_eq!(resp.url(), format!("{}/get?q=1&q=2", *HTTPBIN_URL)); } + +#[wasm_bindgen_test] +async fn request_clone() { + let req = Request::get(&format!("{}/get", *HTTPBIN_URL)) + .build() + .unwrap(); + + let req1 = req.clone(); + let req2 = req.clone(); + + let resp1 = req1.send().await.unwrap(); + let resp2 = req2.send().await.unwrap(); + + assert_eq!(resp1.status(), 200); + assert_eq!(resp2.status(), 200); +} + +#[wasm_bindgen_test] +async fn request_clone_with_body() { + // Get a stream of bytes by making a request + let resp = Request::get(&format!("{}/get", *HTTPBIN_URL)) + .send() + .await + .unwrap(); + + // Build a request with the body of the response + let req = Request::post(&format!("{}/post", *HTTPBIN_URL)) + .body(resp.body()) + .build() + .unwrap(); + + // Clone the request + let req1 = req.clone(); + let req2 = req.clone(); + + // Send both requests + let resp1 = req1.send().await.unwrap(); + let resp2 = req2.send().await.unwrap(); + + assert_eq!(resp1.status(), 200); + assert_eq!(resp2.status(), 200); +} diff --git a/crates/net/tests/query.rs b/crates/net/tests/query.rs index a16e061c..ad8f77fc 100644 --- a/crates/net/tests/query.rs +++ b/crates/net/tests/query.rs @@ -1,3 +1,5 @@ +use std::iter::FromIterator; + use gloo_net::http::QueryParams; use wasm_bindgen_test::*; @@ -5,7 +7,7 @@ wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn query_params_iter() { - let params = QueryParams::new(); + let mut params = QueryParams::new(); params.append("a", "1"); params.append("b", "value"); let mut entries = params.iter(); @@ -16,7 +18,7 @@ fn query_params_iter() { #[wasm_bindgen_test] fn query_params_get() { - let params = QueryParams::new(); + let mut params = QueryParams::new(); params.append("a", "1"); params.append("a", "value"); assert_eq!(params.get("a"), Some("1".to_string())); @@ -29,7 +31,7 @@ fn query_params_get() { #[wasm_bindgen_test] fn query_params_delete() { - let params = QueryParams::new(); + let mut params = QueryParams::new(); params.append("a", "1"); params.append("a", "value"); params.delete("a"); @@ -38,10 +40,37 @@ fn query_params_delete() { #[wasm_bindgen_test] fn query_params_escape() { - let params = QueryParams::new(); + let mut params = QueryParams::new(); params.append("a", "1"); assert_eq!(params.to_string(), "a=1".to_string()); params.append("key", "ab&c"); assert_eq!(params.to_string(), "a=1&key=ab%26c"); } + +#[wasm_bindgen_test] +fn query_clone() { + // Verify that a deep copy is made + let mut params1 = QueryParams::new(); + params1.append("a", "1"); + + let params2 = params1.clone(); + assert_eq!(params1.get("a"), Some("1".to_string())); + assert_eq!(params2.get("a"), Some("1".to_string())); + + params1.append("b", "2"); + assert_eq!(params1.get("b"), Some("2".to_string())); + assert_eq!(params2.get("b"), None); +} + +#[wasm_bindgen_test] +fn query_from_iter() { + // Test from_iter + let mut params = QueryParams::from_iter(vec![("a", "1"), ("b", "2")]); + assert_eq!(params.get("a"), Some("1".to_string())); + assert_eq!(params.get("b"), Some("2".to_string())); + + // Test extend + params.extend(vec![("c", "3")]); + assert_eq!(params.get("c"), Some("3".to_string())); +}