use crate::browser::util;
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, collections::BTreeMap, fmt, str::FromStr};
use wasm_bindgen::JsValue;
pub const DUMMY_BASE_URL: &str = "http://example.com";
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Url {
next_path_part_index: usize,
next_hash_path_part_index: usize,
path: Vec<String>,
hash_path: Vec<String>,
hash: Option<String>,
search: UrlSearch,
invalid_components: Vec<String>,
}
impl Url {
pub fn new() -> Self {
Self::default()
}
pub fn go_and_push(&self) {
let data = JsValue::from_str(
&serde_json::to_string(&self).expect("Problem serializing route data"),
);
util::history()
.push_state_with_url(&data, "", Some(&self.to_string()))
.expect("Problem pushing state");
}
pub fn go_and_replace(&self) {
let data = JsValue::from_str(
&serde_json::to_string(&self).expect("Problem serializing route data"),
);
util::history()
.replace_state_with_url(&data, "", Some(&self.to_string()))
.expect("Problem pushing state");
}
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")
}
pub fn next_path_part(&mut self) -> Option<&str> {
let path_part = self.path.get(self.next_path_part_index);
if path_part.is_some() {
self.next_path_part_index += 1;
}
path_part.map(String::as_str)
}
pub fn next_hash_path_part(&mut self) -> Option<&str> {
let hash_path_part = self.hash_path.get(self.next_hash_path_part_index);
if hash_path_part.is_some() {
self.next_hash_path_part_index += 1;
}
hash_path_part.map(String::as_str)
}
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();
self.path
.iter()
.skip(path_part_index)
.map(String::as_str)
.collect()
}
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();
self.hash_path
.iter()
.skip(hash_path_part_index)
.map(String::as_str)
.collect()
}
pub fn add_path_part(mut self, path_part: impl Into<String>) -> Self {
self.path.push(path_part.into());
self
}
pub fn add_hash_path_part(mut self, hash_path_part: impl Into<String>) -> Self {
self.hash_path.push(hash_path_part.into());
self.hash = Some(self.hash_path.join("/"));
self
}
pub fn to_base_url(&self) -> Self {
let mut url = self.clone();
url.path.truncate(self.next_path_part_index);
url
}
pub fn to_hash_base_url(&self) -> Self {
let mut url = self.clone();
url.hash_path.truncate(self.next_hash_path_part_index);
url
}
pub fn set_path<T: ToString>(
mut self,
into_path_iterator: impl IntoIterator<Item = T>,
) -> Self {
self.path = into_path_iterator
.into_iter()
.map(|p| p.to_string())
.collect();
self.next_path_part_index = 0;
self
}
pub fn set_hash_path<T: ToString>(
mut self,
into_hash_path_iterator: impl IntoIterator<Item = T>,
) -> 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
}
pub fn set_hash(mut self, hash: impl Into<String>) -> Self {
let hash = hash.into();
self.hash_path = hash.split('/').map(ToOwned::to_owned).collect();
self.hash = Some(hash);
self
}
pub fn set_search(mut self, search: impl Into<UrlSearch>) -> Self {
self.search = search.into();
self
}
pub fn path(&self) -> &[String] {
&self.path
}
pub fn hash_path(&self) -> &[String] {
&self.path
}
pub fn hash(&self) -> Option<&String> {
self.hash.as_ref()
}
pub const fn search(&self) -> &UrlSearch {
&self.search
}
pub fn search_mut(&mut self) -> &mut UrlSearch {
&mut self.search
}
pub fn go_and_load(&self) {
util::window()
.location()
.set_href(&self.to_string())
.expect("set location href");
}
pub fn go_and_load_with_str(url: impl AsRef<str>) {
util::window()
.location()
.set_href(url.as_ref())
.expect("set location href");
}
pub fn reload() {
util::window().location().reload().expect("reload location");
}
pub fn reload_and_skip_cache() {
util::window()
.location()
.reload_with_forceget(true)
.expect("reload location with forceget");
}
pub fn go_back(steps: i32) {
util::history().go_with_delta(-steps).expect("go back");
}
pub fn go_forward(steps: i32) {
util::history().go_with_delta(steps).expect("go forward");
}
pub fn 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
}
pub fn decode_uri_component(component: impl AsRef<str>) -> Result<String, JsValue> {
let decoded = js_sys::decode_uri_component(component.as_ref())?;
Ok(String::from(decoded))
}
pub fn invalid_components(&self) -> &[String] {
&self.invalid_components
}
pub fn invalid_components_mut(&mut self) -> &mut Vec<String> {
&mut self.invalid_components
}
}
impl fmt::Display for Url {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
let url = web_sys::Url::new_with_base(&self.path.join("/"), DUMMY_BASE_URL)
.expect("create native url");
url.set_search(&self.search.to_string());
if let Some(hash) = &self.hash {
url.set_hash(hash);
}
write!(fmt, "{}", &url.href().trim_start_matches(DUMMY_BASE_URL))
}
}
impl<'a> From<&'a Url> for Cow<'a, Url> {
fn from(url: &'a Url) -> Cow<'a, Url> {
Cow::Borrowed(url)
}
}
impl<'a> From<Url> for Cow<'a, Url> {
fn from(url: Url) -> Cow<'a, Url> {
Cow::Owned(url)
}
}
impl FromStr for Url {
type Err = String;
fn from_str(str_url: &str) -> Result<Self, Self::Err> {
web_sys::Url::new_with_base(str_url, DUMMY_BASE_URL)
.map(|url| Url::from(&url))
.map_err(|_| format!("`{}` is invalid relative URL", str_url))
}
}
impl From<&web_sys::Url> for Url {
fn from(url: &web_sys::Url) -> Self {
let mut invalid_components = Vec::<String>::new();
let path = {
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)
}
})
.collect::<Vec<_>>()
};
let hash = {
let mut hash = url.hash();
if hash.is_empty() {
None
} else {
hash.remove(0);
let hash = match Url::decode_uri_component(&hash) {
Ok(decoded_hash) => decoded_hash,
Err(_) => {
invalid_components.push(hash.clone());
hash
}
};
Some(hash)
}
};
let hash_path = {
let mut hash = url.hash();
if hash.is_empty() {
Vec::new()
} else {
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::<Vec<_>>()
}
};
let search = UrlSearch::from(url.search_params());
invalid_components.append(&mut search.invalid_components.clone());
Self {
next_path_part_index: 0,
next_hash_path_part_index: 0,
path,
hash_path,
hash,
search,
invalid_components,
}
}
}
#[allow(clippy::module_name_repetitions)]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UrlSearch {
search: BTreeMap<String, Vec<String>>,
invalid_components: Vec<String>,
}
impl UrlSearch {
pub fn new<K, V, VS>(params: impl IntoIterator<Item = (K, VS)>) -> Self
where
K: Into<String>,
V: Into<String>,
VS: IntoIterator<Item = V>,
{
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(),
}
}
pub fn contains_key(&self, key: impl AsRef<str>) -> bool {
self.search.contains_key(key.as_ref())
}
pub fn get(&self, key: impl AsRef<str>) -> Option<&Vec<String>> {
self.search.get(key.as_ref())
}
pub fn get_mut(&mut self, key: impl AsRef<str>) -> Option<&mut Vec<String>> {
self.search.get_mut(key.as_ref())
}
pub fn push_value<'a>(&mut self, key: impl Into<Cow<'a, str>>, 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]);
}
}
pub fn insert(&mut self, key: String, values: Vec<String>) -> Option<Vec<String>> {
self.search.insert(key, values)
}
pub fn remove(&mut self, key: impl AsRef<str>) -> Option<Vec<String>> {
self.search.remove(key.as_ref())
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<String>)> {
self.search.iter()
}
pub fn invalid_components(&self) -> &[String] {
&self.invalid_components
}
pub fn invalid_components_mut(&mut self) -> &mut Vec<String> {
&mut self.invalid_components
}
}
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<web_sys::UrlSearchParams> for UrlSearch {
fn from(params: web_sys::UrlSearchParams) -> Self {
let mut url_search = Self::default();
let mut invalid_components = Vec::<String>::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
}
}
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
use super::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn parse_url_decoding() {
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);
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"]),])
);
assert_eq!(url.hash(), Some(&"heš".to_owned()));
let actual = url.to_string();
assert_eq!(expected, actual)
}
#[wasm_bindgen_test]
fn parse_url_path() {
let expected = Url::new().set_path(&["path1", "path2"]);
let actual: Url = "/path1/path2".parse().unwrap();
assert_eq!(expected, actual)
}
#[wasm_bindgen_test]
fn parse_url_with_hash_search() {
let expected = Url::new()
.set_path(&["path"])
.set_search(UrlSearch::new(vec![("search", vec!["query"])]))
.set_hash("hash");
let actual: Url = "/path?search=query#hash".parse().unwrap();
assert_eq!(expected, actual)
}
#[wasm_bindgen_test]
fn parse_url_with_hash_only() {
let expected = Url::new().set_path(&["path"]).set_hash("hash");
let actual: Url = "/path#hash".parse().unwrap();
assert_eq!(expected, actual)
}
#[wasm_bindgen_test]
fn parse_url_with_hash_routing() {
let expected = Url::new().set_hash_path(&["discover"]);
let actual: Url = "/#discover".parse().unwrap();
assert_eq!(expected, actual)
}
#[wasm_bindgen_test]
fn check_url_to_string() {
let expected = "/foo/bar?q=42&z=13#discover";
let actual = Url::new()
.set_path(&["foo", "bar"])
.set_search(UrlSearch::new(vec![("q", vec!["42"]), ("z", vec!["13"])]))
.set_hash_path(&["discover"])
.to_string();
assert_eq!(expected, actual)
}
}