From b70441bab08efe7ed0aa58f5205da899e1659d41 Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Fri, 29 May 2020 21:35:14 -0500 Subject: [PATCH 01/12] Extract `UrlSearch` struct into a submodule. --- src/browser/url.rs | 160 +------------------------------------- src/browser/url/search.rs | 159 +++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 156 deletions(-) create mode 100644 src/browser/url/search.rs diff --git a/src/browser/url.rs b/src/browser/url.rs index 329c207d3..2cd5fbb77 100644 --- a/src/browser/url.rs +++ b/src/browser/url.rs @@ -1,8 +1,11 @@ use crate::browser::util; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, collections::BTreeMap, fmt, str::FromStr}; +use std::{borrow::Cow, fmt, str::FromStr}; use wasm_bindgen::JsValue; +mod search; +pub use search::UrlSearch; + pub const DUMMY_BASE_URL: &str = "http://example.com"; // ------ Url ------ @@ -551,161 +554,6 @@ impl From<&web_sys::Url> for Url { } } -// ------ UrlSearch ------ - -#[allow(clippy::module_name_repetitions)] -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct UrlSearch { - search: BTreeMap>, - invalid_components: Vec, -} - -impl UrlSearch { - /// Makes a new `UrlSearch` with the provided parameters. - /// - /// # Examples - /// - /// ```rust,no_run - /// UrlSearch::new(vec![ - /// ("sort", vec!["date", "name"]), - /// ("category", vec!["top"]) - /// ]) - /// ``` - pub fn new(params: impl IntoIterator) -> Self - where - K: Into, - V: Into, - VS: IntoIterator, - { - let mut search = BTreeMap::new(); - for (key, values) in params { - search.insert(key.into(), values.into_iter().map(Into::into).collect()); - } - Self { - search, - invalid_components: Vec::new(), - } - } - - /// Returns `true` if the `UrlSearch` contains a value for the specified key. - pub fn contains_key(&self, key: impl AsRef) -> bool { - self.search.contains_key(key.as_ref()) - } - - /// Returns a reference to values corresponding to the key. - pub fn get(&self, key: impl AsRef) -> Option<&Vec> { - self.search.get(key.as_ref()) - } - - /// Returns a mutable reference to values corresponding to the key. - pub fn get_mut(&mut self, key: impl AsRef) -> Option<&mut Vec> { - self.search.get_mut(key.as_ref()) - } - - /// Push the value into the vector of values corresponding to the key. - /// - If the key and values are not present, they will be crated. - pub fn push_value<'a>(&mut self, key: impl Into>, value: String) { - let key = key.into(); - if self.search.contains_key(key.as_ref()) { - self.search.get_mut(key.as_ref()).unwrap().push(value); - } else { - self.search.insert(key.into_owned(), vec![value]); - } - } - - /// Inserts a key-values pair into the `UrlSearch`. - /// - If the `UrlSearch` did not have this key present, `None` is returned. - /// - If the `UrlSearch` did have this key present, old values are overwritten by new ones, - /// and old values are returned. The key is not updated. - pub fn insert(&mut self, key: String, values: Vec) -> Option> { - self.search.insert(key, values) - } - - /// Removes a key from the `UrlSearch`, returning values at the key - /// if the key was previously in the `UrlSearch`. - pub fn remove(&mut self, key: impl AsRef) -> Option> { - self.search.remove(key.as_ref()) - } - - /// Gets an iterator over the entries of the `UrlSearch`, sorted by key. - pub fn iter(&self) -> impl Iterator)> { - self.search.iter() - } - - /// Get invalid components. - /// - /// Undecodable / unparsable components are invalid. - pub fn invalid_components(&self) -> &[String] { - &self.invalid_components - } - - /// Get mutable invalid components. - /// - /// Undecodable / unparsable components are invalid. - pub fn invalid_components_mut(&mut self) -> &mut Vec { - &mut self.invalid_components - } -} - -/// `UrlSearch` components are automatically encoded. -impl fmt::Display for UrlSearch { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - let params = web_sys::UrlSearchParams::new().expect("create a new UrlSearchParams"); - - for (key, values) in &self.search { - for value in values { - params.append(key, value); - } - } - write!(fmt, "{}", String::from(params.to_string())) - } -} - -impl From for UrlSearch { - /// Creates a new `UrlSearch` from the browser native `UrlSearchParams`. - /// `UrlSearch`'s components are decoded if possible. When decoding fails, the component is cloned - /// into `invalid_components` and the original value is used. - fn from(params: web_sys::UrlSearchParams) -> Self { - let mut url_search = Self::default(); - let mut invalid_components = Vec::::new(); - - for param in js_sys::Array::from(¶ms).to_vec() { - let key_value_pair = js_sys::Array::from(¶m).to_vec(); - - let key = key_value_pair - .get(0) - .expect("get UrlSearchParams key from key-value pair") - .as_string() - .expect("cast UrlSearchParams key to String"); - let value = key_value_pair - .get(1) - .expect("get UrlSearchParams value from key-value pair") - .as_string() - .expect("cast UrlSearchParams value to String"); - - let key = match Url::decode_uri_component(&key) { - Ok(decoded_key) => decoded_key, - Err(_) => { - invalid_components.push(key.clone()); - key - } - }; - let value = match Url::decode_uri_component(&value) { - Ok(decoded_value) => decoded_value, - Err(_) => { - invalid_components.push(value.clone()); - value - } - }; - - url_search.push_value(key, value) - } - - url_search.invalid_components = invalid_components; - url_search - } -} - // ------ ------ Tests ------ ------ #[cfg(test)] diff --git a/src/browser/url/search.rs b/src/browser/url/search.rs new file mode 100644 index 000000000..f5a7d158f --- /dev/null +++ b/src/browser/url/search.rs @@ -0,0 +1,159 @@ +use serde::{Deserialize, Serialize}; +use std::{borrow::Cow, collections::BTreeMap, fmt}; + +use super::Url; + +// ------ UrlSearch ------ + +#[allow(clippy::module_name_repetitions)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct UrlSearch { + search: BTreeMap>, + pub(super) invalid_components: Vec, +} + +impl UrlSearch { + /// Makes a new `UrlSearch` with the provided parameters. + /// + /// # Examples + /// + /// ```rust,no_run + /// UrlSearch::new(vec![ + /// ("sort", vec!["date", "name"]), + /// ("category", vec!["top"]) + /// ]) + /// ``` + pub fn new(params: impl IntoIterator) -> Self + where + K: Into, + V: Into, + VS: IntoIterator, + { + let mut search = BTreeMap::new(); + for (key, values) in params { + search.insert(key.into(), values.into_iter().map(Into::into).collect()); + } + Self { + search, + invalid_components: Vec::new(), + } + } + + /// Returns `true` if the `UrlSearch` contains a value for the specified key. + pub fn contains_key(&self, key: impl AsRef) -> bool { + self.search.contains_key(key.as_ref()) + } + + /// Returns a reference to values corresponding to the key. + pub fn get(&self, key: impl AsRef) -> Option<&Vec> { + self.search.get(key.as_ref()) + } + + /// Returns a mutable reference to values corresponding to the key. + pub fn get_mut(&mut self, key: impl AsRef) -> Option<&mut Vec> { + self.search.get_mut(key.as_ref()) + } + + /// Push the value into the vector of values corresponding to the key. + /// - If the key and values are not present, they will be crated. + pub fn push_value<'a>(&mut self, key: impl Into>, value: String) { + let key = key.into(); + if self.search.contains_key(key.as_ref()) { + self.search.get_mut(key.as_ref()).unwrap().push(value); + } else { + self.search.insert(key.into_owned(), vec![value]); + } + } + + /// Inserts a key-values pair into the `UrlSearch`. + /// - If the `UrlSearch` did not have this key present, `None` is returned. + /// - If the `UrlSearch` did have this key present, old values are overwritten by new ones, + /// and old values are returned. The key is not updated. + pub fn insert(&mut self, key: String, values: Vec) -> Option> { + self.search.insert(key, values) + } + + /// Removes a key from the `UrlSearch`, returning values at the key + /// if the key was previously in the `UrlSearch`. + pub fn remove(&mut self, key: impl AsRef) -> Option> { + self.search.remove(key.as_ref()) + } + + /// Gets an iterator over the entries of the `UrlSearch`, sorted by key. + pub fn iter(&self) -> impl Iterator)> { + self.search.iter() + } + + /// Get invalid components. + /// + /// Undecodable / unparsable components are invalid. + pub fn invalid_components(&self) -> &[String] { + &self.invalid_components + } + + /// Get mutable invalid components. + /// + /// Undecodable / unparsable components are invalid. + pub fn invalid_components_mut(&mut self) -> &mut Vec { + &mut self.invalid_components + } +} + +/// `UrlSearch` components are automatically encoded. +impl fmt::Display for UrlSearch { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let params = web_sys::UrlSearchParams::new().expect("create a new UrlSearchParams"); + + for (key, values) in &self.search { + for value in values { + params.append(key, value); + } + } + write!(fmt, "{}", String::from(params.to_string())) + } +} + +impl From for UrlSearch { + /// Creates a new `UrlSearch` from the browser native `UrlSearchParams`. + /// `UrlSearch`'s components are decoded if possible. When decoding fails, the component is cloned + /// into `invalid_components` and the original value is used. + fn from(params: web_sys::UrlSearchParams) -> Self { + let mut url_search = Self::default(); + let mut invalid_components = Vec::::new(); + + for param in js_sys::Array::from(¶ms).to_vec() { + let key_value_pair = js_sys::Array::from(¶m).to_vec(); + + let key = key_value_pair + .get(0) + .expect("get UrlSearchParams key from key-value pair") + .as_string() + .expect("cast UrlSearchParams key to String"); + let value = key_value_pair + .get(1) + .expect("get UrlSearchParams value from key-value pair") + .as_string() + .expect("cast UrlSearchParams value to String"); + + let key = match Url::decode_uri_component(&key) { + Ok(decoded_key) => decoded_key, + Err(_) => { + invalid_components.push(key.clone()); + key + } + }; + let value = match Url::decode_uri_component(&value) { + Ok(decoded_value) => decoded_value, + Err(_) => { + invalid_components.push(value.clone()); + value + } + }; + + url_search.push_value(key, value) + } + + url_search.invalid_components = invalid_components; + url_search + } +} From fb3ca39bb89183c1b0f1a049c6c17c4b71b4608f Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Fri, 29 May 2020 22:11:02 -0500 Subject: [PATCH 02/12] Split methods in Url by purpose - constructors - getters - setters - browser actions involving `Url` - browser actions independent of `Url` - manipulation of what I'm calling the `base_path` and `active_path` for now - A "miscellaneous" section that only has one function (decoding percent encoded strings) Marked a few TODOs for thinking about, not immediately actionable. --- src/browser/url.rs | 401 ++++++++++++++++++++++++--------------------- 1 file changed, 215 insertions(+), 186 deletions(-) diff --git a/src/browser/url.rs b/src/browser/url.rs index 2cd5fbb77..5956b34d8 100644 --- a/src/browser/url.rs +++ b/src/browser/url.rs @@ -29,12 +29,166 @@ pub struct Url { invalid_components: Vec, } +// Constructors + impl Url { /// Creates a new `Url` with the empty path. pub fn new() -> Self { Self::default() } + /// Creates a new `Url` from the one that is currently set in the browser. + pub fn current() -> Url { + let current_url = util::window().location().href().expect("get `href`"); + Url::from_str(¤t_url).expect("create `web_sys::Url` from the current URL") + } +} + +// Getters + +impl Url { + /// Get path. + /// + /// # Refenences + /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname) + pub fn path(&self) -> &[String] { + &self.path + } + + /// Get hash path. + pub fn hash_path(&self) -> &[String] { + &self.path + } + + /// Get hash. + /// + /// # References + /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) + pub fn hash(&self) -> Option<&String> { + self.hash.as_ref() + } + + /// Get search. + /// + /// # Refenences + /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/search) + pub const fn search(&self) -> &UrlSearch { + &self.search + } + + /// Get mutable search. + /// + /// # Refenences + /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/search) + pub fn search_mut(&mut self) -> &mut UrlSearch { + &mut self.search + } + + /// Get invalid components. + /// + /// Undecodable / unparsable components are invalid. + pub fn invalid_components(&self) -> &[String] { + &self.invalid_components + } + + /// Get mutable invalid components. + /// + /// Undecodable / unparsable components are invalid. + pub fn invalid_components_mut(&mut self) -> &mut Vec { + &mut self.invalid_components + } +} + +// Setters + +impl Url { + /// Sets path and returns updated `Url`. It also resets internal path iterator. + /// + /// # Example + /// + /// ```rust, no_run + /// Url::new().set_path(&["my", "path"]) + /// ``` + /// + /// # Refenences + /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname) + pub fn set_path( + mut self, + into_path_iterator: impl IntoIterator, + ) -> Self { + self.path = into_path_iterator + .into_iter() + .map(|p| p.to_string()) + .collect(); + self.next_path_part_index = 0; + self + } + + /// Sets hash path and returns updated `Url`. + /// It also resets internal hash path iterator and sets `hash`. + /// + /// # Example + /// + /// ```rust, no_run + /// Url::new().set_hash_path(&["my", "path"]) + /// ``` + /// + /// # Refenences + /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname) + pub fn set_hash_path( + mut self, + into_hash_path_iterator: impl IntoIterator, + ) -> Self { + self.hash_path = into_hash_path_iterator + .into_iter() + .map(|p| p.to_string()) + .collect(); + self.next_hash_path_part_index = 0; + self.hash = Some(self.hash_path.join("/")); + self + } + + /// Sets hash and returns updated `Url`. + /// I also sets `hash_path`. + /// + /// # Example + /// + /// ```rust, no_run + /// Url::new().set_hash("my_hash") + /// ``` + /// + /// # References + /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) + pub fn set_hash(mut self, hash: impl Into) -> Self { + let hash = hash.into(); + self.hash_path = hash.split('/').map(ToOwned::to_owned).collect(); + self.hash = Some(hash); + self + } + + /// Sets search and returns updated `Url`. + /// + /// # Example + /// + /// ```rust, no_run + /// Url::new().set_search(UrlSearch::new(vec![ + /// ("x", vec!["1"]), + /// ("sort_by", vec!["date", "name"]), + /// ]) + /// ``` + /// + /// # Refenences + /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/search) + pub fn set_search(mut self, search: impl Into) -> Self { + self.search = search.into(); + self + } +} + +// Browser actions dependent on the Url struct +// TODO: Consider moving all Browser actions into a separate `routing` module. + +impl Url { /// Change the browser URL, but do not trigger a page load. /// /// This will add a new entry to the browser history. @@ -68,13 +222,65 @@ impl Url { .replace_state_with_url(&data, "", Some(&self.to_string())) .expect("Problem pushing state"); } + + /// Change the browser URL and trigger a page load. + pub fn go_and_load(&self) { + util::window() + .location() + .set_href(&self.to_string()) + .expect("set location href"); + } +} - /// Creates a new `Url` from the one that is currently set in the browser. - pub fn current() -> Url { - let current_url = util::window().location().href().expect("get `href`"); - Url::from_str(¤t_url).expect("create `web_sys::Url` from the current URL") +// Actions independent of the Url struct +// TODO: consider making these free functions + +impl Url { + /// Change the browser URL and trigger a page load. + /// + /// Provided `url` isn't checked and it's passed into `location.href`. + pub fn go_and_load_with_str(url: impl AsRef) { + util::window() + .location() + .set_href(url.as_ref()) + .expect("set location href"); + } + + /// Trigger a page reload. + pub fn reload() { + util::window().location().reload().expect("reload location"); + } + + /// Trigger a page reload and force reloading from the server. + pub fn reload_and_skip_cache() { + util::window() + .location() + .reload_with_forceget(true) + .expect("reload location with forceget"); + } + + /// Move back in `History`. + /// + /// - `steps: 0` only reloads the current page. + /// - Negative steps move you forward - use rather `Url::go_forward` instead. + /// - If there is no previous page, this call does nothing. + pub fn go_back(steps: i32) { + util::history().go_with_delta(-steps).expect("go back"); + } + + /// Move back in `History`. + /// + /// - `steps: 0` only reloads the current page. + /// - Negative steps move you back - use rather `Url::go_back` instead. + /// - If there is no next page, this call does nothing. + pub fn go_forward(steps: i32) { + util::history().go_with_delta(steps).expect("go forward"); } +} + +// Url `base_path`/`active_path` manipulation +impl Url { /// Advances the internal path iterator and returns the next path part as `Option<&str>`. /// /// # Example @@ -205,174 +411,6 @@ impl Url { url } - /// Sets path and returns updated `Url`. It also resets internal path iterator. - /// - /// # Example - /// - /// ```rust, no_run - /// Url::new().set_path(&["my", "path"]) - /// ``` - /// - /// # Refenences - /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname) - pub fn set_path( - mut self, - into_path_iterator: impl IntoIterator, - ) -> Self { - self.path = into_path_iterator - .into_iter() - .map(|p| p.to_string()) - .collect(); - self.next_path_part_index = 0; - self - } - - /// Sets hash path and returns updated `Url`. - /// It also resets internal hash path iterator and sets `hash`. - /// - /// # Example - /// - /// ```rust, no_run - /// Url::new().set_hash_path(&["my", "path"]) - /// ``` - /// - /// # Refenences - /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname) - pub fn set_hash_path( - mut self, - into_hash_path_iterator: impl IntoIterator, - ) -> Self { - self.hash_path = into_hash_path_iterator - .into_iter() - .map(|p| p.to_string()) - .collect(); - self.next_hash_path_part_index = 0; - self.hash = Some(self.hash_path.join("/")); - self - } - - /// Sets hash and returns updated `Url`. - /// I also sets `hash_path`. - /// - /// # Example - /// - /// ```rust, no_run - /// Url::new().set_hash("my_hash") - /// ``` - /// - /// # References - /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) - pub fn set_hash(mut self, hash: impl Into) -> Self { - let hash = hash.into(); - self.hash_path = hash.split('/').map(ToOwned::to_owned).collect(); - self.hash = Some(hash); - self - } - - /// Sets search and returns updated `Url`. - /// - /// # Example - /// - /// ```rust, no_run - /// Url::new().set_search(UrlSearch::new(vec![ - /// ("x", vec!["1"]), - /// ("sort_by", vec!["date", "name"]), - /// ]) - /// ``` - /// - /// # Refenences - /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/search) - pub fn set_search(mut self, search: impl Into) -> Self { - self.search = search.into(); - self - } - - /// Get path. - /// - /// # Refenences - /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname) - pub fn path(&self) -> &[String] { - &self.path - } - - /// Get hash path. - pub fn hash_path(&self) -> &[String] { - &self.path - } - - /// Get hash. - /// - /// # References - /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) - pub fn hash(&self) -> Option<&String> { - self.hash.as_ref() - } - - /// Get search. - /// - /// # Refenences - /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/search) - pub const fn search(&self) -> &UrlSearch { - &self.search - } - - /// Get mutable search. - /// - /// # Refenences - /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/search) - pub fn search_mut(&mut self) -> &mut UrlSearch { - &mut self.search - } - - /// Change the browser URL and trigger a page load. - pub fn go_and_load(&self) { - util::window() - .location() - .set_href(&self.to_string()) - .expect("set location href"); - } - - /// Change the browser URL and trigger a page load. - /// - /// Provided `url` isn't checked and it's passed into `location.href`. - pub fn go_and_load_with_str(url: impl AsRef) { - util::window() - .location() - .set_href(url.as_ref()) - .expect("set location href"); - } - - /// Trigger a page reload. - pub fn reload() { - util::window().location().reload().expect("reload location"); - } - - /// Trigger a page reload and force reloading from the server. - pub fn reload_and_skip_cache() { - util::window() - .location() - .reload_with_forceget(true) - .expect("reload location with forceget"); - } - - /// Move back in `History`. - /// - /// - `steps: 0` only reloads the current page. - /// - Negative steps move you forward - use rather `Url::go_forward` instead. - /// - If there is no previous page, this call does nothing. - pub fn go_back(steps: i32) { - util::history().go_with_delta(-steps).expect("go back"); - } - - /// Move back in `History`. - /// - /// - `steps: 0` only reloads the current page. - /// - Negative steps move you back - use rather `Url::go_back` instead. - /// - If there is no next page, this call does nothing. - pub fn go_forward(steps: i32) { - util::history().go_with_delta(steps).expect("go forward"); - } - /// If the current `Url`'s path prefix is equal to `path_base`, /// then reset the internal path iterator and advance it to skip the prefix (aka `path_base`). /// @@ -384,7 +422,12 @@ impl Url { } self } +} + +// Things that don't fit +// TODO: consider making this a free floating function, making it private, or both. +impl Url { /// Decodes a Uniform Resource Identifier (URI) component. /// Aka percent-decoding. /// @@ -405,20 +448,6 @@ impl Url { let decoded = js_sys::decode_uri_component(component.as_ref())?; Ok(String::from(decoded)) } - - /// Get invalid components. - /// - /// Undecodable / unparsable components are invalid. - pub fn invalid_components(&self) -> &[String] { - &self.invalid_components - } - - /// Get mutable invalid components. - /// - /// Undecodable / unparsable components are invalid. - pub fn invalid_components_mut(&mut self) -> &mut Vec { - &mut self.invalid_components - } } /// `Url` components are automatically encoded. From 13cab297fe417aed8e3236d72c487967025f4aaa Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Fri, 29 May 2020 22:31:35 -0500 Subject: [PATCH 03/12] Flatten + tweak Small change, will be mostly untouched in the rest of the changes. Attempt to lessen the rightward "drift" of the code. Also since `filter_map` is rarely used. --- src/browser/url.rs | 80 ++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 46 deletions(-) diff --git a/src/browser/url.rs b/src/browser/url.rs index 5956b34d8..a8aa714cd 100644 --- a/src/browser/url.rs +++ b/src/browser/url.rs @@ -504,67 +504,55 @@ impl From<&web_sys::Url> for Url { fn from(url: &web_sys::Url) -> Self { let mut invalid_components = Vec::::new(); - let path = { + let path: Vec<_> = { let path = url.pathname(); path.split('/') - .filter_map(|path_part| { - if path_part.is_empty() { - None - } else { - let path_part = match Url::decode_uri_component(path_part) { - Ok(decoded_path_part) => decoded_path_part, - Err(_) => { - invalid_components.push(path_part.to_owned()); - path_part.to_owned() - } - }; - Some(path_part) + .filter(|path_part| !path_part.is_empty()) + .map(|path_part| { + match Url::decode_uri_component(path_part) { + Ok(decoded_path_part) => decoded_path_part, + Err(_) => { + invalid_components.push(path_part.to_owned()); + path_part.to_string() + } } }) - .collect::>() + .collect() }; - let hash = { - let mut hash = url.hash(); + let (hash, hash_path) = { + let hash = url.hash(); if hash.is_empty() { - None + (None, Vec::new()) } else { // Remove leading `#`. - hash.remove(0); + let hash = &hash['#'.len_utf8()..]; + + // Decode hash path parts. + let hash_path = hash.split('/') + .filter(|path_part| !path_part.is_empty()) + .map(|path_part| { + match Url::decode_uri_component(path_part) { + Ok(decoded_path_part) => decoded_path_part, + Err(_) => { + invalid_components.push(path_part.to_owned()); + path_part.to_owned() + } + } + }) + .collect(); + + // Decode hash. let hash = match Url::decode_uri_component(&hash) { Ok(decoded_hash) => decoded_hash, Err(_) => { - invalid_components.push(hash.clone()); - hash + invalid_components.push(hash.to_owned()); + hash.to_owned() } }; - Some(hash) - } - }; - let hash_path = { - let mut hash = url.hash(); - if hash.is_empty() { - Vec::new() - } else { - // Remove leading `#`. - hash.remove(0); - hash.split('/') - .filter_map(|path_part| { - if path_part.is_empty() { - None - } else { - let path_part = match Url::decode_uri_component(path_part) { - Ok(decoded_path_part) => decoded_path_part, - Err(_) => { - invalid_components.push(path_part.to_owned()); - path_part.to_owned() - } - }; - Some(path_part) - } - }) - .collect::>() + // Return `(hash, hash_path)` + (Some(hash), hash_path) } }; From 5587f294287720967f4eefef89dda11caae31f50 Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Sat, 30 May 2020 00:11:05 -0500 Subject: [PATCH 04/12] Update examples so that they compile. rename everything in terms of `base` path and `relative` path pop/push to maintain consistency with `Vec` (potentially rename pop to shift for clarity) try added to note that the attempt can fail `set_hash` changed to depend on `set_hash_path` `go_and_load` changed to depend on `go_and_load_with_str` --- src/app.rs | 2 +- src/browser/service/routing.rs | 6 +- src/browser/url.rs | 210 ++++++++++++++++++++++----------- 3 files changed, 143 insertions(+), 75 deletions(-) diff --git a/src/app.rs b/src/app.rs index 014b05ec1..4df93da82 100644 --- a/src/app.rs +++ b/src/app.rs @@ -184,7 +184,7 @@ impl + 'static, GMs: 'static> App| -> AfterMount { - let url = url.skip_base_path(&base_path); + let url = url.try_skip_base_path(&base_path); let model = init(url, orders); AfterMount::new(model).url_handling(UrlHandling::None) } diff --git a/src/browser/service/routing.rs b/src/browser/service/routing.rs index cc3e25aa9..455626e4b 100644 --- a/src/browser/service/routing.rs +++ b/src/browser/service/routing.rs @@ -46,7 +46,7 @@ pub fn setup_popstate_listener( }; notify(Notification::new(subs::UrlChanged( - url.clone().skip_base_path(&base_path), + url.clone().try_skip_base_path(&base_path), ))); if let Some(routes) = routes { @@ -85,7 +85,7 @@ pub fn setup_hashchange_listener( .expect("cast hashchange event url to `Url`"); notify(Notification::new(subs::UrlChanged( - url.clone().skip_base_path(&base_path), + url.clone().try_skip_base_path(&base_path), ))); if let Some(routes) = routes { @@ -117,7 +117,7 @@ pub(crate) fn url_request_handler( event.prevent_default(); // Prevent page refresh } notify(Notification::new(subs::UrlChanged( - url.skip_base_path(&base_path), + url.try_skip_base_path(&base_path), ))); } subs::url_requested::UrlRequestStatus::Handled(prevent_default) => { diff --git a/src/browser/url.rs b/src/browser/url.rs index a8aa714cd..c0c09f0d1 100644 --- a/src/browser/url.rs +++ b/src/browser/url.rs @@ -10,18 +10,30 @@ pub const DUMMY_BASE_URL: &str = "http://example.com"; // ------ Url ------ -/// URL used for routing. +/// URL used for routing. The struct also keeps track of the "base" path vs the "relative" path components +/// within the URL. The relative path appended to the base path forms the "absolute" path or simply, the +/// path. For example: +/// +/// ```text +/// https://site.com/albums/seedlings/oak-45.png +/// ^base^ ^----relative------^ +/// ^---------absolute--------^ +/// ``` +/// +/// Note that methods exist to change which parts of the URL are considered the +/// "base" vs the "relative" parts. This concept also applies for "hash paths". /// /// - It represents relative URL. -/// - Two, almost identical, `Url`s that differ only with differently advanced -/// internal path or hash path iterators (e.g. `next_path_part()` was called on one of them) -/// are considered different also during comparison. +/// - Two `Url`s that represent the same absolute path but different base/relative +/// paths (e.g. `pop_path_part()` was called on one of them) are considered +/// different when compared. /// -/// (If the features above are problems for you, create an [issue](https://github.com/seed-rs/seed/issues/new)) +/// (If the features above are problems for you, please [create an issue on our +/// GitHub page](https://github.com/seed-rs/seed/issues/new). Thank you!) #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub struct Url { - next_path_part_index: usize, - next_hash_path_part_index: usize, + base_path_len: usize, + base_hash_path_len: usize, path: Vec, hash_path: Vec, hash: Option, @@ -47,7 +59,7 @@ impl Url { // Getters impl Url { - /// Get path. + /// Get the (absolute) path. /// /// # Refenences /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname) @@ -55,12 +67,12 @@ impl Url { &self.path } - /// Get hash path. + /// Get the hash path. pub fn hash_path(&self) -> &[String] { &self.path } - /// Get hash. + /// Get the hash. /// /// # References /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) @@ -68,7 +80,7 @@ impl Url { self.hash.as_ref() } - /// Get search. + /// Get the search parameters. /// /// # Refenences /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/search) @@ -76,7 +88,7 @@ impl Url { &self.search } - /// Get mutable search. + /// Get a mutable version of the search parameters. /// /// # Refenences /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/search) @@ -84,14 +96,14 @@ impl Url { &mut self.search } - /// Get invalid components. + /// Get the invalid components. /// /// Undecodable / unparsable components are invalid. pub fn invalid_components(&self) -> &[String] { &self.invalid_components } - /// Get mutable invalid components. + /// Get a mutable version of the invalid components. /// /// Undecodable / unparsable components are invalid. pub fn invalid_components_mut(&mut self) -> &mut Vec { @@ -102,7 +114,9 @@ impl Url { // Setters impl Url { - /// Sets path and returns updated `Url`. It also resets internal path iterator. + /// Sets the (absolute) path and returns the updated `Url`. + /// + /// It also resets the base and relative paths. /// /// # Example /// @@ -120,12 +134,13 @@ impl Url { .into_iter() .map(|p| p.to_string()) .collect(); - self.next_path_part_index = 0; + self.base_path_len = 0; self } - /// Sets hash path and returns updated `Url`. - /// It also resets internal hash path iterator and sets `hash`. + /// Sets the (absolute) hash path and returns the updated `Url`. + /// + /// It also resets the base and relative hash paths and sets `hash`. /// /// # Example /// @@ -143,13 +158,14 @@ impl Url { .into_iter() .map(|p| p.to_string()) .collect(); - self.next_hash_path_part_index = 0; + self.base_hash_path_len = 0; self.hash = Some(self.hash_path.join("/")); self } - /// Sets hash and returns updated `Url`. - /// I also sets `hash_path`. + /// Sets the hash and returns the updated `Url`. + /// + /// It also sets the hash path, effectively calling `set_hash_path`. /// /// # Example /// @@ -159,14 +175,13 @@ impl Url { /// /// # References /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) - pub fn set_hash(mut self, hash: impl Into) -> Self { - let hash = hash.into(); - self.hash_path = hash.split('/').map(ToOwned::to_owned).collect(); - self.hash = Some(hash); - self + pub fn set_hash(self, hash: impl Into) -> Self { + // TODO: Probably not an issue, but this effectively clones `hash` once. + // TODO: Optionally implement a private function to handle both. + self.set_hash_path(hash.into().split('/')) } - /// Sets search and returns updated `Url`. + /// Sets the search parameters and returns the updated `Url`. /// /// # Example /// @@ -225,10 +240,7 @@ impl Url { /// Change the browser URL and trigger a page load. pub fn go_and_load(&self) { - util::window() - .location() - .set_href(&self.to_string()) - .expect("set location href"); + Self::go_and_load_with_str(self.to_string()) } } @@ -238,7 +250,7 @@ impl Url { impl Url { /// Change the browser URL and trigger a page load. /// - /// Provided `url` isn't checked and it's passed into `location.href`. + /// Provided `url` isn't checked and directly set to `location.href`. pub fn go_and_load_with_str(url: impl AsRef) { util::window() .location() @@ -281,50 +293,86 @@ impl Url { // Url `base_path`/`active_path` manipulation impl Url { - /// Advances the internal path iterator and returns the next path part as `Option<&str>`. + /// Returns the first part of the relative path and advances the base path. + /// Moves the first part of the relative path into the base path and returns + /// a reference to the moved portion. /// - /// # Example + /// The effects are as follows. Before: + /// + /// ```text + /// https://site.com/albums/seedlings/oak-45.png + /// ^base^ ^----relative------^ + /// ^---------absolute--------^ + /// ``` + /// + /// and after: + /// + /// ```text + /// https://site.com/albums/seedlings/oak-45.png + /// ^-----base-----^ ^relative^ + /// ^---------absolute--------^ + /// ``` + /// + /// # Code example /// /// ```rust,no_run - ///match url.next_path_part() { + ///match url.advance_base_path() { /// None => Page::Home, /// Some("report") => Page::Report(page::report::init(url)), /// _ => Page::Unknown(url), ///} /// ```` - pub fn next_path_part(&mut self) -> Option<&str> { - let path_part = self.path.get(self.next_path_part_index); + pub fn pop_relative_path_part(&mut self) -> Option<&str> { + let path_part = self.path.get(self.base_path_len); if path_part.is_some() { - self.next_path_part_index += 1; + self.base_path_len += 1; } path_part.map(String::as_str) } - /// Advances the internal hash path iterator and returns the next hash path part as `Option<&str>`. + /// Moves the first part of the relative hash path into the base hash path + /// and returns a reference to the moved portion, similar to `pop_relative_path`. /// /// # Example /// /// ```rust,no_run - ///match url.next_hash_path_part() { + ///match url.pop_relative_hash_path() { /// None => Page::Home, /// Some("report") => Page::Report(page::report::init(url)), /// _ => Page::Unknown(url), ///} /// ```` - pub fn next_hash_path_part(&mut self) -> Option<&str> { - let hash_path_part = self.hash_path.get(self.next_hash_path_part_index); + pub fn pop_relative_hash_path_part(&mut self) -> Option<&str> { + let hash_path_part = self.hash_path.get(self.base_hash_path_len); if hash_path_part.is_some() { - self.next_hash_path_part_index += 1; + self.base_hash_path_len += 1; } hash_path_part.map(String::as_str) } - /// Collects the internal path iterator and returns it as `Vec<&str>`. + /// Moves all the components of the relative path to the base path and + /// returns them as `Vec<&str>`. + /// + /// The effects are as follows. Before: + /// + /// ```text + /// https://site.com/albums/seedlings/oak-45.png + /// ^base^ ^----relative------^ + /// ^---------absolute--------^ + /// ``` + /// + /// and after: + /// + /// ```text + /// https://site.com/albums/seedlings/oak-45.png + /// ^-----------base----------^ + /// ^---------absolute--------^ + /// ``` /// /// # Example /// /// ```rust,no_run - ///match url.remaining_path_parts().as_slice() { + ///match url.consume_relative_path().as_slice() { /// [] => Page::Home, /// ["report", rest @ ..] => { /// match rest { @@ -335,9 +383,9 @@ impl Url { /// _ => Page::NotFound, ///} /// ```` - pub fn remaining_path_parts(&mut self) -> Vec<&str> { - let path_part_index = self.next_path_part_index; - self.next_path_part_index = self.path.len(); + pub fn consume_relative_path(&mut self) -> Vec<&str> { + let path_part_index = self.base_path_len; + self.base_path_len = self.path.len(); self.path .iter() .skip(path_part_index) @@ -345,12 +393,13 @@ impl Url { .collect() } - /// Collects the internal hash path iterator and returns it as `Vec<&str>`. + /// Moves all the components of the relative hash path to the base hash path + /// and returns them as `Vec<&str>`, similar to `consume_hash_path`. /// /// # Example /// /// ```rust,no_run - ///match url.remaining_hash_path_parts().as_slice() { + ///match url.consume_relative_hash_path().as_slice() { /// [] => Page::Home, /// ["report", rest @ ..] => { /// match rest { @@ -361,9 +410,9 @@ impl Url { /// _ => Page::NotFound, ///} /// ```` - pub fn remaining_hash_path_parts(&mut self) -> Vec<&str> { - let hash_path_part_index = self.next_hash_path_part_index; - self.next_hash_path_part_index = self.hash_path.len(); + pub fn consume_relative_hash_path(&mut self) -> Vec<&str> { + let hash_path_part_index = self.base_hash_path_len; + self.base_hash_path_len = self.hash_path.len(); self.hash_path .iter() .skip(hash_path_part_index) @@ -371,54 +420,73 @@ impl Url { .collect() } - /// Adds given path part and returns updated `Url`. + /// Adds the given path part and returns the updated `Url`. The path + /// part is added to the relative path. /// /// # Example /// /// ```rust,no_run - ///let link_to_blog = url.add_path_part("blog"); + ///let link_to_blog = url.push_path_part("blog"); /// ```` - pub fn add_path_part(mut self, path_part: impl Into) -> Self { + pub fn push_path_part(mut self, path_part: impl Into) -> Self { self.path.push(path_part.into()); self } - /// Adds given hash path part and returns updated `Url`. + /// Adds the given hash path part and returns the updated `Url`. /// It also changes `hash`. /// /// # Example /// /// ```rust,no_run - ///let link_to_blog = url.add_hash_path_part("blog"); + ///let link_to_blog = url.push_hash_path_part("blog"); /// ```` - pub fn add_hash_path_part(mut self, hash_path_part: impl Into) -> Self { + pub fn push_hash_path_part(mut self, hash_path_part: impl Into) -> Self { self.hash_path.push(hash_path_part.into()); self.hash = Some(self.hash_path.join("/")); self } - /// Clone the `Url` and strip remaining path parts. - pub fn to_base_url(&self) -> Self { + /// Clone the `Url` and strip relative path. + /// + /// The effects are as follows. Input: + /// + /// ```text + /// https://site.com/albums/seedlings/oak-45.png + /// ^-----base-----^ ^relative^ + /// ^---------absolute--------^ + /// ``` + /// + /// and output: + /// + /// ```text + /// https://site.com/albums/seedlings + /// ^-----base-----^ + /// ^---absolute---^ + /// ``` + pub fn truncate_relative_path(&mut self) -> Self { let mut url = self.clone(); - url.path.truncate(self.next_path_part_index); + url.path.truncate(self.base_path_len); url } - /// Clone the `Url` and strip remaining hash path parts. - pub fn to_hash_base_url(&self) -> Self { + /// Clone the `Url` and strip relative hash path. Similar to + /// `truncate_relative_path`. + pub fn truncate_relative_hash_path(&self) -> Self { let mut url = self.clone(); - url.hash_path.truncate(self.next_hash_path_part_index); + url.hash_path.truncate(self.base_hash_path_len); url } - /// If the current `Url`'s path prefix is equal to `path_base`, - /// then reset the internal path iterator and advance it to skip the prefix (aka `path_base`). + /// If the current `Url`'s path starts with `path_base`, then set the base + /// path to the provided `path_base` and the rest to the relative path. /// /// It's used mostly by Seed internals, but it can be useful in combination /// with `orders.clone_base_path()`. - pub fn skip_base_path(mut self, path_base: &[String]) -> Self { + // TODO potentially return `Result` so that the user can act on the check. + pub fn try_skip_base_path(mut self, path_base: &[String]) -> Self { if self.path.starts_with(path_base) { - self.next_path_part_index = path_base.len(); + self.base_path_len = path_base.len(); } self } @@ -560,8 +628,8 @@ impl From<&web_sys::Url> for Url { invalid_components.append(&mut search.invalid_components.clone()); Self { - next_path_part_index: 0, - next_hash_path_part_index: 0, + base_path_len: 0, + base_hash_path_len: 0, path, hash_path, hash, From 7661ec2f3e3bcc11c074af2d8b22c90065d99094 Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Sat, 30 May 2020 00:15:28 -0500 Subject: [PATCH 05/12] Update examples to match rename --- examples/auth/src/lib.rs | 6 +++--- examples/pages/src/lib.rs | 6 +++--- examples/pages/src/page/admin.rs | 4 ++-- examples/pages/src/page/admin/page/report.rs | 8 ++++---- examples/pages_hash_routing/src/lib.rs | 6 +++--- examples/pages_hash_routing/src/page/admin.rs | 4 ++-- examples/pages_hash_routing/src/page/admin/page/report.rs | 8 ++++---- examples/pages_keep_state/src/lib.rs | 6 +++--- examples/pages_keep_state/src/page/admin.rs | 4 ++-- examples/pages_keep_state/src/page/admin/page/report.rs | 8 ++++---- examples/todomvc/src/lib.rs | 2 +- examples/unsaved_changes/src/lib.rs | 4 ++-- examples/url/src/lib.rs | 6 +++--- 13 files changed, 36 insertions(+), 36 deletions(-) diff --git a/examples/auth/src/lib.rs b/examples/auth/src/lib.rs index 1b94f0314..a1d0ce736 100644 --- a/examples/auth/src/lib.rs +++ b/examples/auth/src/lib.rs @@ -18,7 +18,7 @@ fn init(url: Url, orders: &mut impl Orders) -> Model { Model { email: "john@example.com".to_owned(), password: "1234".to_owned(), - base_url: url.to_base_url(), + base_url: url.clone().truncate_relative_path(), page: Page::init(url, user.as_ref(), orders), secret_message: None, user, @@ -59,7 +59,7 @@ enum Page { impl Page { fn init(mut url: Url, user: Option<&LoggedUser>, orders: &mut impl Orders) -> Self { - match url.next_path_part() { + match url.pop_relative_path_part() { None => { if let Some(user) = user { send_request_to_top_secret(user.token.clone(), orders) @@ -99,7 +99,7 @@ impl<'a> Urls<'a> { self.base_url() } pub fn login(self) -> Url { - self.base_url().add_path_part(LOGIN) + self.base_url().push_path_part(LOGIN) } } diff --git a/examples/pages/src/lib.rs b/examples/pages/src/lib.rs index 3701f7643..dc982def8 100644 --- a/examples/pages/src/lib.rs +++ b/examples/pages/src/lib.rs @@ -16,7 +16,7 @@ fn init(url: Url, orders: &mut impl Orders) -> Model { ctx: Context { logged_user: "John Doe", }, - base_url: url.to_base_url(), + base_url: url.clone().truncate_relative_path(), page: Page::init(url), } } @@ -47,7 +47,7 @@ enum Page { impl Page { fn init(mut url: Url) -> Self { - match url.next_path_part() { + match url.pop_relative_path_part() { None => Self::Home, Some(ADMIN) => page::admin::init(url).map_or(Self::NotFound, Self::Admin), _ => Self::NotFound, @@ -65,7 +65,7 @@ impl<'a> Urls<'a> { self.base_url() } pub fn admin_urls(self) -> page::admin::Urls<'a> { - page::admin::Urls::new(self.base_url().add_path_part(ADMIN)) + page::admin::Urls::new(self.base_url().push_path_part(ADMIN)) } } diff --git a/examples/pages/src/page/admin.rs b/examples/pages/src/page/admin.rs index c00190e7a..967899d88 100644 --- a/examples/pages/src/page/admin.rs +++ b/examples/pages/src/page/admin.rs @@ -11,7 +11,7 @@ mod page; pub fn init(mut url: Url) -> Option { Some(Model { - report_page: match url.next_path_part() { + report_page: match url.pop_relative_path_part() { Some(REPORT) => page::report::init(url)?, _ => None?, }, @@ -33,7 +33,7 @@ pub struct Model { struct_urls!(); impl<'a> Urls<'a> { pub fn report_urls(self) -> page::report::Urls<'a> { - page::report::Urls::new(self.base_url().add_path_part(REPORT)) + page::report::Urls::new(self.base_url().push_path_part(REPORT)) } } diff --git a/examples/pages/src/page/admin/page/report.rs b/examples/pages/src/page/admin/page/report.rs index 25f290eee..252c607ff 100644 --- a/examples/pages/src/page/admin/page/report.rs +++ b/examples/pages/src/page/admin/page/report.rs @@ -9,9 +9,9 @@ const WEEKLY: &str = "weekly"; // ------ ------ pub fn init(mut url: Url) -> Option { - let base_url = url.to_base_url(); + let base_url = url.clone().truncate_relative_path(); - let frequency = match url.remaining_path_parts().as_slice() { + let frequency = match url.consume_relative_path().as_slice() { [] => { Urls::new(&base_url).default().go_and_replace(); Frequency::default() @@ -59,10 +59,10 @@ impl<'a> Urls<'a> { self.daily() } pub fn daily(self) -> Url { - self.base_url().add_path_part(DAILY) + self.base_url().push_path_part(DAILY) } pub fn weekly(self) -> Url { - self.base_url().add_path_part(WEEKLY) + self.base_url().push_path_part(WEEKLY) } } diff --git a/examples/pages_hash_routing/src/lib.rs b/examples/pages_hash_routing/src/lib.rs index d77c43ba8..be96838c4 100644 --- a/examples/pages_hash_routing/src/lib.rs +++ b/examples/pages_hash_routing/src/lib.rs @@ -16,7 +16,7 @@ fn init(url: Url, orders: &mut impl Orders) -> Model { ctx: Context { logged_user: "John Doe", }, - base_url: url.to_hash_base_url(), + base_url: url.clone().truncate_relative_hash_path(), page: Page::init(url), } } @@ -47,7 +47,7 @@ enum Page { impl Page { fn init(mut url: Url) -> Self { - match url.next_hash_path_part() { + match url.pop_relative_hash_path_part() { None => Self::Home, Some(ADMIN) => page::admin::init(url).map_or(Self::NotFound, Self::Admin), _ => Self::NotFound, @@ -65,7 +65,7 @@ impl<'a> Urls<'a> { self.base_url() } pub fn admin_urls(self) -> page::admin::Urls<'a> { - page::admin::Urls::new(self.base_url().add_hash_path_part(ADMIN)) + page::admin::Urls::new(self.base_url().push_hash_path_part(ADMIN)) } } diff --git a/examples/pages_hash_routing/src/page/admin.rs b/examples/pages_hash_routing/src/page/admin.rs index 87e972ff4..40242ebbd 100644 --- a/examples/pages_hash_routing/src/page/admin.rs +++ b/examples/pages_hash_routing/src/page/admin.rs @@ -11,7 +11,7 @@ mod page; pub fn init(mut url: Url) -> Option { Some(Model { - report_page: match url.next_hash_path_part() { + report_page: match url.pop_relative_hash_path_part() { Some(REPORT) => page::report::init(url)?, _ => None?, }, @@ -33,7 +33,7 @@ pub struct Model { struct_urls!(); impl<'a> Urls<'a> { pub fn report_urls(self) -> page::report::Urls<'a> { - page::report::Urls::new(self.base_url().add_hash_path_part(REPORT)) + page::report::Urls::new(self.base_url().push_hash_path_part(REPORT)) } } diff --git a/examples/pages_hash_routing/src/page/admin/page/report.rs b/examples/pages_hash_routing/src/page/admin/page/report.rs index 834b1cb87..33d2f766b 100644 --- a/examples/pages_hash_routing/src/page/admin/page/report.rs +++ b/examples/pages_hash_routing/src/page/admin/page/report.rs @@ -9,9 +9,9 @@ const WEEKLY: &str = "weekly"; // ------ ------ pub fn init(mut url: Url) -> Option { - let base_url = url.to_hash_base_url(); + let base_url = url.clone().truncate_relative_hash_path(); - let frequency = match url.remaining_hash_path_parts().as_slice() { + let frequency = match url.consume_relative_hash_path().as_slice() { [] => { Urls::new(&base_url).default().go_and_replace(); Frequency::default() @@ -59,10 +59,10 @@ impl<'a> Urls<'a> { self.daily() } pub fn daily(self) -> Url { - self.base_url().add_hash_path_part(DAILY) + self.base_url().push_hash_path_part(DAILY) } pub fn weekly(self) -> Url { - self.base_url().add_hash_path_part(WEEKLY) + self.base_url().push_hash_path_part(WEEKLY) } } diff --git a/examples/pages_keep_state/src/lib.rs b/examples/pages_keep_state/src/lib.rs index 64359ad8c..1b86ab005 100644 --- a/examples/pages_keep_state/src/lib.rs +++ b/examples/pages_keep_state/src/lib.rs @@ -11,7 +11,7 @@ const ADMIN: &str = "admin"; // ------ ------ fn init(url: Url, orders: &mut impl Orders) -> Model { - let base_url = url.to_base_url(); + let base_url = url.clone().truncate_relative_path(); orders .subscribe(Msg::UrlChanged) .notify(subs::UrlChanged(url)); @@ -61,7 +61,7 @@ impl<'a> Urls<'a> { self.base_url() } pub fn admin_urls(self) -> page::admin::Urls<'a> { - page::admin::Urls::new(self.base_url().add_path_part(ADMIN)) + page::admin::Urls::new(self.base_url().push_path_part(ADMIN)) } } @@ -76,7 +76,7 @@ enum Msg { fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { match msg { Msg::UrlChanged(subs::UrlChanged(mut url)) => { - model.page_id = match url.next_path_part() { + model.page_id = match url.pop_relative_path_part() { None => Some(PageId::Home), Some(ADMIN) => { page::admin::init(url, &mut model.admin_model).map(|_| PageId::Admin) diff --git a/examples/pages_keep_state/src/page/admin.rs b/examples/pages_keep_state/src/page/admin.rs index ded962584..806ab84de 100644 --- a/examples/pages_keep_state/src/page/admin.rs +++ b/examples/pages_keep_state/src/page/admin.rs @@ -11,7 +11,7 @@ mod page; pub fn init(mut url: Url, model: &mut Option) -> Option<()> { let model = model.get_or_insert_with(Model::default); - model.page_id.replace(match url.next_path_part() { + model.page_id.replace(match url.pop_relative_path_part() { Some(REPORT) => page::report::init(url, &mut model.report_model).map(|_| PageId::Report)?, _ => None?, }); @@ -42,7 +42,7 @@ enum PageId { struct_urls!(); impl<'a> Urls<'a> { pub fn report_urls(self) -> page::report::Urls<'a> { - page::report::Urls::new(self.base_url().add_path_part(REPORT)) + page::report::Urls::new(self.base_url().push_path_part(REPORT)) } } diff --git a/examples/pages_keep_state/src/page/admin/page/report.rs b/examples/pages_keep_state/src/page/admin/page/report.rs index 825d2fab2..f6b81a797 100644 --- a/examples/pages_keep_state/src/page/admin/page/report.rs +++ b/examples/pages_keep_state/src/page/admin/page/report.rs @@ -10,11 +10,11 @@ const WEEKLY: &str = "weekly"; pub fn init(mut url: Url, model: &mut Option) -> Option<()> { let model = model.get_or_insert_with(|| Model { - base_url: url.to_base_url(), + base_url: url.clone().truncate_relative_path(), frequency: Frequency::Daily, }); - model.frequency = match url.remaining_path_parts().as_slice() { + model.frequency = match url.consume_relative_path().as_slice() { [] => { match model.frequency { Frequency::Daily => Urls::new(&model.base_url).daily().go_and_replace(), @@ -56,10 +56,10 @@ impl<'a> Urls<'a> { self.base_url() } pub fn daily(self) -> Url { - self.base_url().add_path_part(DAILY) + self.base_url().push_path_part(DAILY) } pub fn weekly(self) -> Url { - self.base_url().add_path_part(WEEKLY) + self.base_url().push_path_part(WEEKLY) } } diff --git a/examples/todomvc/src/lib.rs b/examples/todomvc/src/lib.rs index 29bc91a42..226d158b7 100644 --- a/examples/todomvc/src/lib.rs +++ b/examples/todomvc/src/lib.rs @@ -117,7 +117,7 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { let data = &mut model.data; match msg { Msg::UrlChanged(subs::UrlChanged(mut url)) => { - data.filter = match url.next_path_part() { + data.filter = match url.pop_relative_path_part() { Some(path_part) if path_part == TodoFilter::Active.to_url_path() => { TodoFilter::Active } diff --git a/examples/unsaved_changes/src/lib.rs b/examples/unsaved_changes/src/lib.rs index 48ff062c8..6a389c510 100644 --- a/examples/unsaved_changes/src/lib.rs +++ b/examples/unsaved_changes/src/lib.rs @@ -18,7 +18,7 @@ fn init(url: Url, orders: &mut impl Orders) -> Model { let text = LocalStorage::get(STORAGE_KEY).unwrap_or_default(); Model { - base_url: url.to_base_url(), + base_url: url.clone().truncate_relative_path(), saved_text_hash: calculate_hash(&text), text, } @@ -44,7 +44,7 @@ impl<'a> Urls<'a> { self.base_url() } fn no_home(self) -> Url { - self.base_url().add_path_part("no-home") + self.base_url().push_path_part("no-home") } } diff --git a/examples/url/src/lib.rs b/examples/url/src/lib.rs index b0b5b418c..475205828 100644 --- a/examples/url/src/lib.rs +++ b/examples/url/src/lib.rs @@ -32,10 +32,10 @@ impl Model { Self { base_path, initial_url: url.clone(), - base_url: url.to_base_url(), - next_path_part: url.next_path_part().map(ToOwned::to_owned), + base_url: url.clone().truncate_relative_path(), + next_path_part: url.pop_relative_path_part().map(ToOwned::to_owned), remaining_path_parts: url - .remaining_path_parts() + .consume_relative_path() .into_iter() .map(ToOwned::to_owned) .collect(), From 08d95f0e58c55c8d1e398e19064f726226722d3c Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Sat, 30 May 2020 00:27:00 -0500 Subject: [PATCH 06/12] Implement base_path + relative_path --- src/browser/url.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/browser/url.rs b/src/browser/url.rs index c0c09f0d1..059831be4 100644 --- a/src/browser/url.rs +++ b/src/browser/url.rs @@ -66,6 +66,16 @@ impl Url { pub fn path(&self) -> &[String] { &self.path } + + /// Get the base path. + pub fn base_path(&mut self) -> &[String] { + &self.path[0..self.base_path_len] + } + + /// Get the relative path. + pub fn relative_path(&mut self) -> &[String] { + &self.path[self.base_path_len..] + } /// Get the hash path. pub fn hash_path(&self) -> &[String] { From 960f3cc09a29669f95a5e7b019520ec98774aa66 Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Sat, 30 May 2020 00:31:05 -0500 Subject: [PATCH 07/12] Update `truncate_*` to act in place Also move two functions further down --- src/browser/url.rs | 68 ++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/browser/url.rs b/src/browser/url.rs index 059831be4..703d0da05 100644 --- a/src/browser/url.rs +++ b/src/browser/url.rs @@ -430,33 +430,6 @@ impl Url { .collect() } - /// Adds the given path part and returns the updated `Url`. The path - /// part is added to the relative path. - /// - /// # Example - /// - /// ```rust,no_run - ///let link_to_blog = url.push_path_part("blog"); - /// ```` - pub fn push_path_part(mut self, path_part: impl Into) -> Self { - self.path.push(path_part.into()); - self - } - - /// Adds the given hash path part and returns the updated `Url`. - /// It also changes `hash`. - /// - /// # Example - /// - /// ```rust,no_run - ///let link_to_blog = url.push_hash_path_part("blog"); - /// ```` - pub fn push_hash_path_part(mut self, hash_path_part: impl Into) -> Self { - self.hash_path.push(hash_path_part.into()); - self.hash = Some(self.hash_path.join("/")); - self - } - /// Clone the `Url` and strip relative path. /// /// The effects are as follows. Input: @@ -474,18 +447,16 @@ impl Url { /// ^-----base-----^ /// ^---absolute---^ /// ``` - pub fn truncate_relative_path(&mut self) -> Self { - let mut url = self.clone(); - url.path.truncate(self.base_path_len); - url + pub fn truncate_relative_path(&mut self) -> &mut Self { + self.path.truncate(self.base_path_len); + self } /// Clone the `Url` and strip relative hash path. Similar to /// `truncate_relative_path`. - pub fn truncate_relative_hash_path(&self) -> Self { - let mut url = self.clone(); - url.hash_path.truncate(self.base_hash_path_len); - url + pub fn truncate_relative_hash_path(&mut self) -> &mut Self { + self.hash_path.truncate(self.base_hash_path_len); + self } /// If the current `Url`'s path starts with `path_base`, then set the base @@ -500,6 +471,33 @@ impl Url { } self } + + /// Adds the given path part and returns the updated `Url`. The path + /// part is added to the relative path. + /// + /// # Example + /// + /// ```rust,no_run + ///let link_to_blog = url.push_path_part("blog"); + /// ```` + pub fn push_path_part(mut self, path_part: impl Into) -> Self { + self.path.push(path_part.into()); + self + } + + /// Adds the given hash path part and returns the updated `Url`. + /// It also changes `hash`. + /// + /// # Example + /// + /// ```rust,no_run + ///let link_to_blog = url.push_hash_path_part("blog"); + /// ```` + pub fn push_hash_path_part(mut self, hash_path_part: impl Into) -> Self { + self.hash_path.push(hash_path_part.into()); + self.hash = Some(self.hash_path.join("/")); + self + } } // Things that don't fit From 91d54d4535febf30979c137842e894f35fe0a5cb Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Sat, 30 May 2020 02:03:03 -0500 Subject: [PATCH 08/12] Make `set_search` take a UrlSearch instead of an Into Implicit conversions tend to cause spikes in processing. Also, makes it work more nicely with a later change. --- src/browser/url.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/url.rs b/src/browser/url.rs index 703d0da05..d65bffa0e 100644 --- a/src/browser/url.rs +++ b/src/browser/url.rs @@ -204,7 +204,7 @@ impl Url { /// /// # Refenences /// * [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/search) - pub fn set_search(mut self, search: impl Into) -> Self { + pub fn set_search(mut self, search: UrlSearch) -> Self { self.search = search.into(); self } From ea4227a0816e1db3b8d641a49c85893cebcbcca7 Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Sat, 30 May 2020 02:13:23 -0500 Subject: [PATCH 09/12] Remove ref on `truncate_` Seems to make it hard to work with sometimes. --- examples/pages_keep_state/src/lib.rs | 5 ++--- src/browser/url.rs | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/pages_keep_state/src/lib.rs b/examples/pages_keep_state/src/lib.rs index 1b86ab005..b33c1798f 100644 --- a/examples/pages_keep_state/src/lib.rs +++ b/examples/pages_keep_state/src/lib.rs @@ -11,16 +11,15 @@ const ADMIN: &str = "admin"; // ------ ------ fn init(url: Url, orders: &mut impl Orders) -> Model { - let base_url = url.clone().truncate_relative_path(); orders .subscribe(Msg::UrlChanged) - .notify(subs::UrlChanged(url)); + .notify(subs::UrlChanged(url.clone())); Model { ctx: Context { logged_user: "John Doe", }, - base_url, + base_url: url.truncate_relative_path(), page_id: None, admin_model: None, } diff --git a/src/browser/url.rs b/src/browser/url.rs index d65bffa0e..df07faeb5 100644 --- a/src/browser/url.rs +++ b/src/browser/url.rs @@ -447,14 +447,14 @@ impl Url { /// ^-----base-----^ /// ^---absolute---^ /// ``` - pub fn truncate_relative_path(&mut self) -> &mut Self { + pub fn truncate_relative_path(mut self) -> Self { self.path.truncate(self.base_path_len); self } /// Clone the `Url` and strip relative hash path. Similar to /// `truncate_relative_path`. - pub fn truncate_relative_hash_path(&mut self) -> &mut Self { + pub fn truncate_relative_hash_path(mut self) -> Self { self.hash_path.truncate(self.base_hash_path_len); self } From f5287839ad33f198fed6ca633211b4a80c719aca Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Sat, 30 May 2020 02:16:46 -0500 Subject: [PATCH 10/12] Add `FromIterator` impl for `UrlSearch` Also, repurposed the now redundant `new` method to create an empty `UrlSearch` --- examples/url/src/lib.rs | 4 +-- src/browser/url.rs | 7 +++-- src/browser/url/search.rs | 64 ++++++++++++++++++++++++--------------- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/examples/url/src/lib.rs b/examples/url/src/lib.rs index 475205828..6b9d0f2ec 100644 --- a/examples/url/src/lib.rs +++ b/examples/url/src/lib.rs @@ -75,9 +75,9 @@ fn view(model: &Model) -> Node { ev(Ev::Click, |_| { Url::new() .set_path(&["ui", "a", "b", "c"]) - .set_search(UrlSearch::new(vec![ + .set_search(vec![ ("x", vec!["1"]) - ])) + ].iter().collect()) .set_hash("hash") .go_and_load() }) diff --git a/src/browser/url.rs b/src/browser/url.rs index df07faeb5..8afdfb24b 100644 --- a/src/browser/url.rs +++ b/src/browser/url.rs @@ -664,12 +664,13 @@ mod tests { let expected = "/Hello%20G%C3%BCnter/path2?calc=5%2B6&x=1&x=2#he%C5%A1"; let native_url = web_sys::Url::new_with_base(expected, DUMMY_BASE_URL).unwrap(); let url = Url::from(&native_url); + let expected_search: UrlSearch = vec![("calc", vec!["5+6"]), ("x", vec!["1", "2"]),].into_iter().collect(); assert_eq!(url.path()[0], "Hello Günter"); assert_eq!(url.path()[1], "path2"); assert_eq!( url.search(), - &UrlSearch::new(vec![("calc", vec!["5+6"]), ("x", vec!["1", "2"]),]) + &expected_search, ); assert_eq!(url.hash(), Some(&"heš".to_owned())); @@ -688,7 +689,7 @@ mod tests { fn parse_url_with_hash_search() { let expected = Url::new() .set_path(&["path"]) - .set_search(UrlSearch::new(vec![("search", vec!["query"])])) + .set_search(vec![("search", vec!["query"])].into_iter().collect()) .set_hash("hash"); let actual: Url = "/path?search=query#hash".parse().unwrap(); assert_eq!(expected, actual) @@ -714,7 +715,7 @@ mod tests { let actual = Url::new() .set_path(&["foo", "bar"]) - .set_search(UrlSearch::new(vec![("q", vec!["42"]), ("z", vec!["13"])])) + .set_search(vec![("q", vec!["42"]), ("z", vec!["13"])].into_iter().collect()) .set_hash_path(&["discover"]) .to_string(); diff --git a/src/browser/url/search.rs b/src/browser/url/search.rs index f5a7d158f..a33b25c2d 100644 --- a/src/browser/url/search.rs +++ b/src/browser/url/search.rs @@ -1,10 +1,7 @@ use serde::{Deserialize, Serialize}; use std::{borrow::Cow, collections::BTreeMap, fmt}; - use super::Url; -// ------ UrlSearch ------ - #[allow(clippy::module_name_repetitions)] #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct UrlSearch { @@ -13,28 +10,10 @@ pub struct UrlSearch { } impl UrlSearch { - /// Makes a new `UrlSearch` with the provided parameters. - /// - /// # Examples - /// - /// ```rust,no_run - /// UrlSearch::new(vec![ - /// ("sort", vec!["date", "name"]), - /// ("category", vec!["top"]) - /// ]) - /// ``` - pub fn new(params: impl IntoIterator) -> Self - where - K: Into, - V: Into, - VS: IntoIterator, - { - let mut search = BTreeMap::new(); - for (key, values) in params { - search.insert(key.into(), values.into_iter().map(Into::into).collect()); - } + /// Create an empty `UrlSearch` object. + pub fn new() -> Self { Self { - search, + search: BTreeMap::new(), invalid_components: Vec::new(), } } @@ -157,3 +136,40 @@ impl From for UrlSearch { url_search } } + +impl std::iter::FromIterator<(K, VS)> for UrlSearch + where + K: Into, + V: Into, + VS: IntoIterator, +{ + fn from_iter>(iter: I) -> Self { + let search = iter.into_iter() + .map(|(k, vs)| { + let k = k.into(); + let v: Vec<_> = vs.into_iter().map(Into::into).collect(); + (k, v) + }) + .collect(); + Self { + search, + invalid_components: Vec::new(), + } + } +} + +impl<'a, K, V, VS> std::iter::FromIterator<&'a (K, VS)> for UrlSearch + where + K: 'a, + &'a K: Into, + V: Into, + VS: 'a, + &'a VS: IntoIterator, +{ + fn from_iter>(iter: I) -> Self { + iter.into_iter().map(|(k, vs)| ( + k.into(), + vs.into_iter(), + )).collect() + } +} From e2f4dd1ef569b1f234e8befb24dd5ecef66a6ac0 Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Sat, 30 May 2020 02:18:04 -0500 Subject: [PATCH 11/12] Oops, missed a spot. --- examples/url/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/url/src/lib.rs b/examples/url/src/lib.rs index 6b9d0f2ec..0e58f8222 100644 --- a/examples/url/src/lib.rs +++ b/examples/url/src/lib.rs @@ -77,7 +77,7 @@ fn view(model: &Model) -> Node { .set_path(&["ui", "a", "b", "c"]) .set_search(vec![ ("x", vec!["1"]) - ].iter().collect()) + ].into_iter().collect()) .set_hash("hash") .go_and_load() }) From 7ef5fbf12bf0efe84f70f5d4985f3b2f28aae136 Mon Sep 17 00:00:00 2001 From: Benjamin Xu Date: Sat, 30 May 2020 02:33:15 -0500 Subject: [PATCH 12/12] Running cargo fmt, since I need to figure out how to run cargo make test in the meantime --- src/browser/url.rs | 58 +++++++++++++++++++-------------------- src/browser/url/search.rs | 32 ++++++++++----------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/browser/url.rs b/src/browser/url.rs index 8afdfb24b..3d4e19269 100644 --- a/src/browser/url.rs +++ b/src/browser/url.rs @@ -66,7 +66,7 @@ impl Url { pub fn path(&self) -> &[String] { &self.path } - + /// Get the base path. pub fn base_path(&mut self) -> &[String] { &self.path[0..self.base_path_len] @@ -247,7 +247,7 @@ impl Url { .replace_state_with_url(&data, "", Some(&self.to_string())) .expect("Problem pushing state"); } - + /// Change the browser URL and trigger a page load. pub fn go_and_load(&self) { Self::go_and_load_with_str(self.to_string()) @@ -314,9 +314,9 @@ impl Url { /// ^base^ ^----relative------^ /// ^---------absolute--------^ /// ``` - /// + /// /// and after: - /// + /// /// ```text /// https://site.com/albums/seedlings/oak-45.png /// ^-----base-----^ ^relative^ @@ -370,9 +370,9 @@ impl Url { /// ^base^ ^----relative------^ /// ^---------absolute--------^ /// ``` - /// + /// /// and after: - /// + /// /// ```text /// https://site.com/albums/seedlings/oak-45.png /// ^-----------base----------^ @@ -439,9 +439,9 @@ impl Url { /// ^-----base-----^ ^relative^ /// ^---------absolute--------^ /// ``` - /// + /// /// and output: - /// + /// /// ```text /// https://site.com/albums/seedlings /// ^-----base-----^ @@ -584,13 +584,11 @@ impl From<&web_sys::Url> for Url { let path = url.pathname(); path.split('/') .filter(|path_part| !path_part.is_empty()) - .map(|path_part| { - match Url::decode_uri_component(path_part) { - Ok(decoded_path_part) => decoded_path_part, - Err(_) => { - invalid_components.push(path_part.to_owned()); - path_part.to_string() - } + .map(|path_part| match Url::decode_uri_component(path_part) { + Ok(decoded_path_part) => decoded_path_part, + Err(_) => { + invalid_components.push(path_part.to_owned()); + path_part.to_string() } }) .collect() @@ -605,15 +603,14 @@ impl From<&web_sys::Url> for Url { let hash = &hash['#'.len_utf8()..]; // Decode hash path parts. - let hash_path = hash.split('/') + let hash_path = hash + .split('/') .filter(|path_part| !path_part.is_empty()) - .map(|path_part| { - match Url::decode_uri_component(path_part) { - Ok(decoded_path_part) => decoded_path_part, - Err(_) => { - invalid_components.push(path_part.to_owned()); - path_part.to_owned() - } + .map(|path_part| match Url::decode_uri_component(path_part) { + Ok(decoded_path_part) => decoded_path_part, + Err(_) => { + invalid_components.push(path_part.to_owned()); + path_part.to_owned() } }) .collect(); @@ -664,14 +661,13 @@ mod tests { let expected = "/Hello%20G%C3%BCnter/path2?calc=5%2B6&x=1&x=2#he%C5%A1"; let native_url = web_sys::Url::new_with_base(expected, DUMMY_BASE_URL).unwrap(); let url = Url::from(&native_url); - let expected_search: UrlSearch = vec![("calc", vec!["5+6"]), ("x", vec!["1", "2"]),].into_iter().collect(); + let expected_search: UrlSearch = vec![("calc", vec!["5+6"]), ("x", vec!["1", "2"])] + .into_iter() + .collect(); assert_eq!(url.path()[0], "Hello Günter"); assert_eq!(url.path()[1], "path2"); - assert_eq!( - url.search(), - &expected_search, - ); + assert_eq!(url.search(), &expected_search,); assert_eq!(url.hash(), Some(&"heš".to_owned())); let actual = url.to_string(); @@ -715,7 +711,11 @@ mod tests { let actual = Url::new() .set_path(&["foo", "bar"]) - .set_search(vec![("q", vec!["42"]), ("z", vec!["13"])].into_iter().collect()) + .set_search( + vec![("q", vec!["42"]), ("z", vec!["13"])] + .into_iter() + .collect(), + ) .set_hash_path(&["discover"]) .to_string(); diff --git a/src/browser/url/search.rs b/src/browser/url/search.rs index a33b25c2d..c8bc6b96d 100644 --- a/src/browser/url/search.rs +++ b/src/browser/url/search.rs @@ -1,6 +1,6 @@ +use super::Url; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, collections::BTreeMap, fmt}; -use super::Url; #[allow(clippy::module_name_repetitions)] #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -138,13 +138,14 @@ impl From for UrlSearch { } impl std::iter::FromIterator<(K, VS)> for UrlSearch - where - K: Into, - V: Into, - VS: IntoIterator, +where + K: Into, + V: Into, + VS: IntoIterator, { fn from_iter>(iter: I) -> Self { - let search = iter.into_iter() + let search = iter + .into_iter() .map(|(k, vs)| { let k = k.into(); let v: Vec<_> = vs.into_iter().map(Into::into).collect(); @@ -159,17 +160,16 @@ impl std::iter::FromIterator<(K, VS)> for UrlSearch } impl<'a, K, V, VS> std::iter::FromIterator<&'a (K, VS)> for UrlSearch - where - K: 'a, - &'a K: Into, - V: Into, - VS: 'a, - &'a VS: IntoIterator, +where + K: 'a, + &'a K: Into, + V: Into, + VS: 'a, + &'a VS: IntoIterator, { fn from_iter>(iter: I) -> Self { - iter.into_iter().map(|(k, vs)| ( - k.into(), - vs.into_iter(), - )).collect() + iter.into_iter() + .map(|(k, vs)| (k.into(), vs.into_iter())) + .collect() } }