use std::{
cell::RefCell,
collections::{HashMap, HashSet},
rc::Rc,
};
use crate::utils::{
bytes::get_lossy_string,
class::{iterator_result, live_iterator, IterKind},
primordials::{BasePrimordials, Primordial},
string::get_coerced_defined_string,
};
use rquickjs::{
atom::PredefinedAtom, class::Trace, function::Opt, prelude::This, Array, Class, Coerced, Ctx,
Exception, FromJs, Function, IntoJs, Null, Object, Result, Symbol, Value,
};
use url::Url;
use super::convert_trailing_space;
#[derive(Clone, Trace, rquickjs::JsLifetime)]
#[rquickjs::class]
pub struct URLSearchParams {
#[qjs(skip_trace)]
pub url: Rc<RefCell<Url>>,
}
#[rquickjs::methods(rename_all = "camelCase")]
impl<'js> URLSearchParams {
#[qjs(constructor)]
pub fn new(ctx: Ctx<'js>, init: Opt<Value<'js>>) -> Result<Self> {
if let Some(init) = init.into_inner() {
if init.is_string() {
let string = get_lossy_string(init)?;
return Ok(Self::from_str(string));
} else if init.is_array() {
return Self::from_array(&ctx, unsafe { init.into_array().unwrap_unchecked() });
} else if init.is_object() {
return Self::from_object(&ctx, unsafe { init.into_object().unwrap_unchecked() });
} else if !init.is_undefined() {
return Ok(Self::from_str(Coerced::<String>::from_js(&ctx, init)?.0));
}
}
let url: Url = unsafe { "http://example.com".parse().unwrap_unchecked() };
Ok(URLSearchParams {
url: Rc::new(RefCell::new(url)),
})
}
#[qjs(get)]
pub fn size(&self) -> usize {
self.url.borrow().query_pairs().count()
}
#[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
pub fn to_string_tag() -> &'static str {
stringify!(URLSearchParams)
}
pub fn append(&mut self, key: Coerced<String>, value: Coerced<String>) {
convert_trailing_space(&mut self.url.borrow_mut());
self.url
.borrow_mut()
.query_pairs_mut()
.append_pair(key.as_str(), value.as_str());
self.sync_query();
}
pub fn delete(&mut self, key: Coerced<String>, value: Opt<Value<'js>>) {
convert_trailing_space(&mut self.url.borrow_mut());
let key = key.0;
let value = get_coerced_defined_string(&value.0);
let new_pairs: Vec<_> = self
.url
.borrow()
.query_pairs()
.filter(|(k, v)| {
if let Some(value) = value.as_ref() {
return !(*k == key && *v == *value);
}
*k != key
})
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
if !new_pairs.is_empty() {
self.url
.borrow_mut()
.query_pairs_mut()
.clear()
.extend_pairs(new_pairs);
} else {
self.url.borrow_mut().set_query(None);
}
self.sync_query();
}
pub fn entries(
this: This<Class<'js, URLSearchParams>>,
ctx: Ctx<'js>,
) -> Result<Class<'js, URLSearchParamsIter<'js>>> {
URLSearchParamsIter::new(&ctx, this.0, IterKind::Entries)
}
pub fn for_each(
this: This<Class<'js, URLSearchParams>>,
callback: Function<'js>,
) -> Result<()> {
let mut index = 0;
loop {
let pair = this
.0
.borrow()
.url
.borrow()
.query_pairs()
.nth(index)
.map(|(k, v)| (k.to_string(), v.to_string()));
let Some((k, v)) = pair else {
break;
};
() = callback.call((v, k, this.0.clone()))?;
index += 1;
}
Ok(())
}
pub fn get(&mut self, ctx: Ctx<'js>, key: String) -> Result<Value<'js>> {
match self
.url
.borrow()
.query_pairs()
.find(|(k, _)| *k == key)
.map(|(_, v)| v)
{
Some(value) => value.into_js(&ctx),
None => Null.into_js(&ctx),
}
}
pub fn get_all(&mut self, key: String) -> Vec<String> {
self.url
.borrow()
.query_pairs()
.filter_map(|(k, v)| if k == key { Some(v.to_string()) } else { None })
.collect()
}
pub fn has(&self, key: Coerced<String>, value: Opt<Value<'js>>) -> bool {
let value = get_coerced_defined_string(&value.0);
let key = key.0;
self.url.borrow().query_pairs().any(|(k, v)| {
if let Some(value) = value.as_ref() {
return *k == key && *v == *value;
}
*k == key
})
}
pub fn keys(
this: This<Class<'js, URLSearchParams>>,
ctx: Ctx<'js>,
) -> Result<Class<'js, URLSearchParamsIter<'js>>> {
URLSearchParamsIter::new(&ctx, this.0, IterKind::Keys)
}
pub fn set(&mut self, key: Coerced<String>, value: Coerced<String>) {
convert_trailing_space(&mut self.url.borrow_mut());
let key = key.0;
let value = value.0;
let mut uniques = HashSet::new();
let mut new_query_pairs: Vec<(String, String)> = Vec::new();
for (k, v) in self.url.borrow().query_pairs() {
let value = if k == key {
value.clone()
} else {
v.to_string()
};
let query_pair = (k.to_string(), value);
if uniques.insert(query_pair.clone()) {
new_query_pairs.push(query_pair);
}
}
let query_pair = (key, value);
if uniques.insert(query_pair.clone()) {
new_query_pairs.push(query_pair);
}
self.url
.borrow_mut()
.query_pairs_mut()
.clear()
.extend_pairs(new_query_pairs);
self.sync_query();
}
pub fn sort(&mut self) {
let mut new_pairs: Vec<(String, String)> =
self.url.borrow().query_pairs().into_owned().collect();
new_pairs.sort_by(|(a, _), (b, _)| {
let a_utf16 = a.encode_utf16();
let b_utf16 = b.encode_utf16();
a_utf16.cmp(b_utf16)
});
if new_pairs.is_empty() {
self.url.borrow_mut().set_query(None);
} else {
self.url
.borrow_mut()
.query_pairs_mut()
.clear()
.extend_pairs(new_pairs);
}
self.sync_query();
}
pub fn to_string(&self) -> String {
let url = self.url.borrow();
url.query_pairs().fold(
String::with_capacity(url.query().map_or(0, |q| q.len())),
|mut acc, (key, value)| {
if !acc.is_empty() {
acc.push('&');
}
url::form_urlencoded::byte_serialize(key.as_bytes()).for_each(|b| acc.push_str(b));
acc.push('=');
url::form_urlencoded::byte_serialize(value.as_bytes())
.for_each(|b| acc.push_str(b));
acc
},
)
}
pub fn values(
this: This<Class<'js, URLSearchParams>>,
ctx: Ctx<'js>,
) -> Result<Class<'js, URLSearchParamsIter<'js>>> {
URLSearchParamsIter::new(&ctx, this.0, IterKind::Values)
}
#[qjs(rename = PredefinedAtom::SymbolIterator)]
pub fn iterator(
this: This<Class<'js, URLSearchParams>>,
ctx: Ctx<'js>,
) -> Result<Class<'js, URLSearchParamsIter<'js>>> {
URLSearchParamsIter::new(&ctx, this.0, IterKind::Entries)
}
}
impl<'js> URLSearchParams {
fn read_entry(&self, index: usize, ctx: &Ctx<'js>) -> Result<Option<(Value<'js>, Value<'js>)>> {
let pair = self
.url
.borrow()
.query_pairs()
.nth(index)
.map(|(k, v)| (k.to_string(), v.to_string()));
match pair {
Some((k, v)) => Ok(Some((k.into_js(ctx)?, v.into_js(ctx)?))),
None => Ok(None),
}
}
fn sync_query(&self) {
let query = self.to_string();
let mut url = self.url.borrow_mut();
if query.is_empty() {
url.set_query(None);
} else {
url.set_query(Some(&query));
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(query: String) -> Self {
let query = if !query.starts_with('?') {
["?", &query].concat()
} else {
query
};
let url = unsafe {
"http://example.com"
.parse::<Url>()
.unwrap_unchecked()
.join(&query)
.unwrap_unchecked()
};
Self {
url: Rc::new(RefCell::new(url)),
}
}
pub fn from_url(url: &Rc<RefCell<Url>>) -> Self {
Self {
url: Rc::clone(url),
}
}
pub fn from_array(ctx: &Ctx<'js>, array: Array<'js>) -> Result<Self> {
let mut url: Url = "http://example.com".parse().unwrap();
let query_pairs: Vec<(String, String)> = array
.into_iter()
.map(|value| {
if let Ok(value) = value {
if let Some(pair) = value.as_array() {
if pair.len() == 2 {
let key_val: Value = pair.get(0)?;
let val_val: Value = pair.get(1)?;
let key = if key_val.is_string() {
get_lossy_string(key_val)?
} else {
Coerced::<String>::from_js(ctx, key_val)?.0
};
let value = if val_val.is_string() {
get_lossy_string(val_val)?
} else {
Coerced::<String>::from_js(ctx, val_val)?.0
};
return Ok((key, value));
}
}
};
Err(Exception::throw_type(
ctx,
"Invalid tuple: Each query pair must be an iterable [name, value] tuple",
))
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.collect();
url.query_pairs_mut().extend_pairs(query_pairs);
Ok(Self {
url: Rc::new(RefCell::new(url)),
})
}
pub fn from_object(ctx: &Ctx<'js>, object: Object<'js>) -> Result<Self> {
let iterator = Symbol::iterator(ctx.clone());
if object.contains_key(iterator)? {
let query_pairs: Array = BasePrimordials::get(ctx)?
.function_array_from
.call((object,))?;
return Self::from_array(ctx, query_pairs);
}
let mut url: Url = "http://example.com".parse().unwrap();
let raw_pairs: Vec<(String, String)> = object
.keys::<Value<'js>>()
.map(|key| {
let key = key?;
let key_string = if key.is_string() {
get_lossy_string(key.clone())?
} else {
Coerced::<String>::from_js(ctx, key.clone())?.0
};
let value_val: Value = object.get(key)?;
let value = if value_val.is_string() {
get_lossy_string(value_val)?
} else {
Coerced::<String>::from_js(ctx, value_val)?.0
};
Ok((key_string, value))
})
.collect::<Result<Vec<_>>>()?;
let mut order: Vec<String> = Vec::with_capacity(raw_pairs.len());
let mut map: HashMap<String, String> = HashMap::with_capacity(raw_pairs.len());
for (k, v) in raw_pairs {
if !map.contains_key(&k) {
order.push(k.clone());
}
map.insert(k, v);
}
let query_pairs: Vec<(String, String)> = order
.into_iter()
.map(|k| {
let v = map.remove(&k).unwrap_or_default();
(k, v)
})
.collect();
url.query_pairs_mut().extend_pairs(query_pairs);
Ok(Self {
url: Rc::new(RefCell::new(url)),
})
}
}
#[derive(Trace, rquickjs::JsLifetime)]
#[rquickjs::class]
pub struct URLSearchParamsIter<'js> {
params: Class<'js, URLSearchParams>,
#[qjs(skip_trace)]
index: usize,
#[qjs(skip_trace)]
kind: IterKind,
}
impl<'js> URLSearchParamsIter<'js> {
fn new(
ctx: &Ctx<'js>,
params: Class<'js, URLSearchParams>,
kind: IterKind,
) -> Result<Class<'js, Self>> {
live_iterator(
ctx,
Self {
params,
index: 0,
kind,
},
)
}
}
#[rquickjs::methods]
impl<'js> URLSearchParamsIter<'js> {
fn next(&mut self, ctx: Ctx<'js>) -> Result<Object<'js>> {
let entry = self.params.borrow().read_entry(self.index, &ctx)?;
if entry.is_some() {
self.index += 1;
}
iterator_result(&ctx, self.kind, entry)
}
#[qjs(rename = PredefinedAtom::SymbolIterator)]
fn iter(this: This<Class<'js, Self>>) -> Class<'js, Self> {
this.0
}
}
#[cfg(test)]
mod tests {
use crate::test::test_sync_with;
use rquickjs::{CatchResultExt, Class};
use super::*;
fn setup(ctx: &rquickjs::Ctx) {
BasePrimordials::init(ctx).unwrap();
Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
}
#[tokio::test]
async fn test_basic() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
params.append('b', '4');
params.append('c', 8);
params.delete('a');
params.delete('b', '2');
params.toString()
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "b=4&c=8");
Ok(())
})
.await
}
#[tokio::test]
async fn test_iterate() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
let res = [];
for (const [name, value] of params) {
res.push(`${name}=${value}`);
}
res.join('&')
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "a=1&b=2&a=3");
Ok(())
})
.await
}
#[tokio::test]
async fn test_iterate_live_delete() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams("foo=0&baz=1&BAR=2");
const keys = [];
for (const [name] of params) {
keys.push(name);
params.delete("baz");
}
keys.join(",")
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "foo,BAR");
Ok(())
})
.await
}
#[tokio::test]
async fn test_iterate_live_append() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams("foo=0&baz=1");
const keys = [];
for (const [name] of params) {
keys.push(name);
if (name === "baz") params.append("end", "9");
}
keys.join(",")
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "foo,baz,end");
Ok(())
})
.await
}
#[tokio::test]
async fn test_iterate_keys_values_live() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams("a=1&b=2&c=3");
const k = [...params.keys()].join(",");
const v = [...params.values()].join(",");
`${k}|${v}`
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "a,b,c|1,2,3");
Ok(())
})
.await
}
#[tokio::test]
async fn test_iterate_entries() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
let res = [];
for (const [name, value] of params.entries()) {
res.push(`${name}=${value}`);
}
res.join('&')
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "a=1&b=2&a=3");
Ok(())
})
.await
}
#[tokio::test]
async fn test_iterate_keys() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
let res = [];
for (const name of params.keys()) {
res.push(name);
}
res.join('&')
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "a&b&a");
Ok(())
})
.await
}
#[tokio::test]
async fn test_iterate_values() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
let res = [];
for (const name of params.values()) {
res.push(name);
}
res.join('&')
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "1&2&3");
Ok(())
})
.await
}
#[tokio::test]
async fn test_new_string() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams('a=1&b=2&a=3');
params.toString()
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "a=1&b=2&a=3");
Ok(())
})
.await
}
#[tokio::test]
async fn test_new_string_url() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams('https://google.com?a=1&b=2&a=3');
params.toString()
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "https%3A%2F%2Fgoogle.com%3Fa=1&b=2&a=3");
Ok(())
})
.await
}
#[tokio::test]
async fn test_new_object() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams({'a': 1, 'b': 2});
params.toString()
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "a=1&b=2");
Ok(())
})
.await
}
#[tokio::test]
async fn test_new_array() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams([['a', 1], ['b', 2], ['a', 3]]);
params.toString()
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "a=1&b=2&a=3");
Ok(())
})
.await
}
#[tokio::test]
async fn test_new_iterator() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
const params2 = new URLSearchParams(params.entries());
params2.toString()
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "a=1&b=2&a=3");
Ok(())
})
.await
}
#[tokio::test]
async fn test_size() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<usize, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
params.size
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, 3);
Ok(())
})
.await
}
#[tokio::test]
async fn test_set() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
params.set('a', '4');
params.toString()
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "a=4&b=2");
Ok(())
})
.await
}
#[tokio::test]
async fn test_get() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
params.get('a')
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "1");
Ok(())
})
.await
}
#[tokio::test]
async fn test_get_missing() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<bool, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
params.get('c') === null
"#,
)
.catch(&ctx)
.unwrap();
assert!(result);
Ok(())
})
.await
}
#[tokio::test]
async fn test_get_all() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
params.getAll('a').join('&')
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "1&3");
Ok(())
})
.await
}
#[tokio::test]
async fn test_get_all_missing() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
params.getAll('c').join('&')
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "");
Ok(())
})
.await
}
#[tokio::test]
async fn test_has() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<bool, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
params.has('b')
"#,
)
.catch(&ctx)
.unwrap();
assert!(result);
Ok(())
})
.await
}
#[tokio::test]
async fn test_has_value() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<bool, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
params.has('b', 5)
"#,
)
.catch(&ctx)
.unwrap();
assert!(!result);
Ok(())
})
.await
}
#[tokio::test]
async fn test_has_not() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<bool, _>(
r#"
const params = new URLSearchParams();
params.append('a', '1');
params.append('b', '2');
params.append('a', '3');
params.has('c')
"#,
)
.catch(&ctx)
.unwrap();
assert!(!result);
Ok(())
})
.await
}
#[tokio::test]
async fn test_sort() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '3');
params.append('b', '2');
params.append('a', '1');
params.sort();
params.toString()
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "a=3&a=1&b=2");
Ok(())
})
.await
}
#[tokio::test]
async fn test_for_each() {
test_sync_with(|ctx| {
setup(&ctx);
let result = ctx
.eval::<String, _>(
r#"
const params = new URLSearchParams();
params.append('a', '3');
params.append('b', '2');
params.append('a', '1');
let res = [];
params.forEach((value, name) => {
res.push(`${name}=${value}`);
});
res.join('&')
"#,
)
.catch(&ctx)
.unwrap();
assert_eq!(result, "a=3&b=2&a=1");
Ok(())
})
.await
}
}