1use either::Either;
2use rquickjs::{
3 Array, Coerced, Ctx, Error, FromJs, Function, JsLifetime, Null, Object, Result, Value,
4 atom::PredefinedAtom,
5 class::Trace,
6 function::{Func, IntoArgs, Opt, This},
7};
8
9#[derive(Default, Clone, Trace, JsLifetime)]
11#[rquickjs::class]
12pub struct URLSearchParams {
13 data: Vec<(String, String)>,
14}
15
16#[rquickjs::methods(rename_all = "camelCase")]
17impl URLSearchParams {
18 #[qjs(constructor)]
20 fn new(input: Opt<URLSearchParamsInput<'_>>) -> Result<Self> {
21 let Some(data) = input.0 else {
22 return Ok(Self::default());
23 };
24
25 let data = match data {
26 URLSearchParamsInput::String(url) => {
27 let query = match url.split_once('?') {
28 Some((_, query)) => query,
29 None => &url,
30 };
31 query
32 .split('&')
33 .map(|part| {
34 let mut parts = part.splitn(2, '=');
35 let name = parts.next().unwrap_or_default().to_string();
36 let value = parts.next().unwrap_or_default().to_string();
37 (name, value)
38 })
39 .collect()
40 }
41 URLSearchParamsInput::Array(array) => {
42 let mut data = Vec::new();
43 for it in array {
44 let inner = it?.get::<Array<'_>>()?;
45 let name = inner.get::<Coerced<String>>(0)?;
46 let value = inner.get::<Coerced<String>>(1)?;
47 data.push((name.0, value.0));
48 }
49 data
50 }
51 URLSearchParamsInput::Object(iter_or_record) => {
52 match iter_or_record.get::<_, Function>(PredefinedAtom::SymbolIterator) {
53 Ok(iter_fn) => {
54 let iterable =
55 (This(iter_or_record.clone()), 2).apply::<Object<'_>>(&iter_fn)?;
56 let next_fn = iterable.get::<_, Function>(PredefinedAtom::Next)?;
57 let mut data = Vec::new();
58 loop {
59 let next = (This(iterable.clone()), 2).apply::<Object<'_>>(&next_fn)?;
60 if let Ok(done) = next.get::<_, bool>(PredefinedAtom::Done) {
61 if done {
62 break;
63 }
64 }
65 let value = next.get::<_, Array<'_>>("value")?;
66 let name = value.get::<Coerced<String>>(0)?;
67 let value = value.get::<Coerced<String>>(1)?;
68 data.push((name.0, value.0));
69 }
70 data
71 }
72 Err(_) => {
73 let mut data = Vec::new();
74 for it in iter_or_record {
75 let (name, value) = it?;
76 let name = name.to_string()?;
77 let value = value.get::<Coerced<String>>()?;
78 data.push((name, value.0));
79 }
80 data
81 }
82 }
83 }
84 };
85
86 Ok(Self { data })
87 }
88
89 #[qjs(rename = PredefinedAtom::SymbolIterator)]
91 pub fn iterate<'js>(&self, ctx: Ctx<'js>, this: This<Self>) -> Result<Object<'js>> {
92 self.entries(ctx, this)
93 }
94
95 fn append(&mut self, name: Coerced<String>, value: Coerced<String>) {
97 self.data.push((name.0, value.0));
98 }
99
100 fn delete(&mut self, name: Coerced<String>, value: Opt<Coerced<String>>) {
102 self.data.retain(|(n, v)| {
103 if n == &name.0 {
104 if let Some(value) = &value.0 {
105 v != &value.0
106 } else {
107 false
108 }
109 } else {
110 true
111 }
112 });
113 }
114
115 pub fn entries<'js>(&self, ctx: Ctx<'js>, this: This<Self>) -> Result<Object<'js>> {
117 let res = Object::new(ctx)?;
118
119 res.set("position", 0usize)?;
120 res.set(
121 PredefinedAtom::SymbolIterator,
122 Func::from(|it: This<Object<'js>>| -> Result<Object<'js>> { Ok(it.0) }),
123 )?;
124 res.set(
125 PredefinedAtom::Next,
126 Func::from(
127 move |ctx: Ctx<'js>, it: This<Object<'js>>| -> Result<Object<'js>> {
128 let position = it.get::<_, usize>("position")?;
129 let res = Object::new(ctx.clone())?;
130 if this.data.len() <= position {
131 res.set(PredefinedAtom::Done, true)?;
132 } else {
133 let (name, value) = &this.data[position];
134 res.set(
135 "value",
136 vec![
137 rquickjs::String::from_str(ctx.clone(), name),
138 rquickjs::String::from_str(ctx, value),
139 ],
140 )?;
141 it.set("position", position + 1)?;
142 }
143 Ok(res)
144 },
145 ),
146 )?;
147 Ok(res)
148 }
149
150 fn for_each<'js>(&self, ctx: Ctx<'js>, callback: Function<'js>) -> Result<()> {
152 for (name, value) in &self.data {
153 let ctx = ctx.clone();
154 callback.call::<_, ()>((
155 rquickjs::String::from_str(ctx.clone(), name),
156 rquickjs::String::from_str(ctx, value),
157 ))?;
158 }
159 Ok(())
160 }
161
162 fn get<'js>(
164 &self,
165 ctx: Ctx<'js>,
166 name: Coerced<String>,
167 ) -> Result<Either<rquickjs::String<'js>, Null>> {
168 let Some((_, value)) = self.data.iter().find(|(n, _)| n == &name.0) else {
169 return Ok(Either::Right(Null));
170 };
171 Ok(Either::Left(rquickjs::String::from_str(ctx, value)?))
172 }
173
174 fn get_all<'js>(
176 &self,
177 ctx: Ctx<'js>,
178 name: Coerced<String>,
179 ) -> Result<Vec<rquickjs::String<'js>>> {
180 let values = self
181 .data
182 .iter()
183 .filter(|(n, _)| n == &name.0)
184 .map(|(_, v)| rquickjs::String::from_str(ctx.clone(), v))
185 .collect::<Result<Vec<_>>>()?;
186 Ok(values)
187 }
188
189 fn has(&self, name: Coerced<String>, value: Opt<Coerced<String>>) -> bool {
191 self.data.iter().any(|(n, v)| {
192 if n == &name.0 {
193 if let Some(value) = &value.0 {
194 v == &value.0
195 } else {
196 true
197 }
198 } else {
199 false
200 }
201 })
202 }
203
204 pub fn keys<'js>(&self, ctx: Ctx<'js>, this: This<Self>) -> Result<Object<'js>> {
206 let res = Object::new(ctx)?;
207
208 res.set("position", 0usize)?;
209 res.set(
210 PredefinedAtom::SymbolIterator,
211 Func::from(|it: This<Object<'js>>| -> Result<Object<'js>> { Ok(it.0) }),
212 )?;
213 res.set(
214 PredefinedAtom::Next,
215 Func::from(
216 move |ctx: Ctx<'js>, it: This<Object<'js>>| -> Result<Object<'js>> {
217 let position = it.get::<_, usize>("position")?;
218 let res = Object::new(ctx.clone())?;
219 if this.data.len() <= position {
220 res.set(PredefinedAtom::Done, true)?;
221 } else {
222 let (name, _) = &this.data[position];
223 res.set("value", rquickjs::String::from_str(ctx, name))?;
224 it.set("position", position + 1)?;
225 }
226 Ok(res)
227 },
228 ),
229 )?;
230 Ok(res)
231 }
232
233 #[qjs(get)]
235 fn size(&self) -> usize {
236 self.data.len()
237 }
238
239 fn set(&mut self, name: Coerced<String>, mut value: Coerced<String>) {
241 let mut found = false;
242 self.data.retain_mut(|(n, v)| {
243 if n == &name.0 {
244 if !found {
245 std::mem::swap(v, &mut value.0);
246 found = true;
247 true
248 } else {
249 false
250 }
251 } else {
252 true
253 }
254 });
255 }
256
257 fn sort(&mut self) {
259 self.data.sort_by(|(a, _), (b, _)| a.cmp(b));
260 }
261
262 #[allow(clippy::inherent_to_string)]
264 fn to_string(&self) -> String {
265 self.data
266 .iter()
267 .map(|(name, value)| format!("{name}={value}"))
268 .collect::<Vec<_>>()
269 .join("&")
270 }
271
272 pub fn values<'js>(&self, ctx: Ctx<'js>, this: This<Self>) -> Result<Object<'js>> {
274 let res = Object::new(ctx)?;
275
276 res.set("position", 0usize)?;
277 res.set(
278 PredefinedAtom::SymbolIterator,
279 Func::from(|it: This<Object<'js>>| -> Result<Object<'js>> { Ok(it.0) }),
280 )?;
281 res.set(
282 PredefinedAtom::Next,
283 Func::from(
284 move |ctx: Ctx<'js>, it: This<Object<'js>>| -> Result<Object<'js>> {
285 let position = it.get::<_, usize>("position")?;
286 let res = Object::new(ctx.clone())?;
287 if this.data.len() <= position {
288 res.set(PredefinedAtom::Done, true)?;
289 } else {
290 let (_, value) = &this.data[position];
291 res.set("value", rquickjs::String::from_str(ctx, value))?;
292 it.set("position", position + 1)?;
293 }
294 Ok(res)
295 },
296 ),
297 )?;
298 Ok(res)
299 }
300}
301
302enum URLSearchParamsInput<'js> {
303 String(String),
304 Array(Array<'js>),
305 Object(Object<'js>),
306}
307
308impl<'js> FromJs<'js> for URLSearchParamsInput<'js> {
309 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
310 if let Ok(string) = value.get::<String>() {
311 Ok(Self::String(string))
312 } else if let Ok(array) = value.get::<Array<'js>>() {
313 Ok(Self::Array(array))
314 } else if let Ok(object) = value.get::<Object<'js>>() {
315 Ok(Self::Object(object))
316 } else {
317 Err(Error::new_from_js(
318 value.type_name(),
319 "URLSearchParamsInput",
320 ))
321 }
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use rquickjs::{CatchResultExt, Class};
328 use rquickjs_extra_test::test_with;
329
330 use super::*;
331
332 #[test]
333 fn test_basic() {
334 test_with(|ctx| {
335 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
336 let result = ctx
337 .eval::<String, _>(
338 r#"
339 const params = new URLSearchParams();
340 params.append('a', '1');
341 params.append('b', '2');
342 params.append('a', '3');
343 params.append('b', '4');
344 params.append('c', 8);
345 params.delete('a');
346 params.delete('b', '2');
347 params.toString()
348 "#,
349 )
350 .catch(&ctx)
351 .unwrap();
352 assert_eq!(result, "b=4&c=8");
353 })
354 }
355
356 #[test]
357 fn test_iterate() {
358 test_with(|ctx| {
359 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
360 let result = ctx
361 .eval::<String, _>(
362 r#"
363 const params = new URLSearchParams();
364 params.append('a', '1');
365 params.append('b', '2');
366 params.append('a', '3');
367 let res = [];
368 for (const [name, value] of params) {
369 res.push(`${name}=${value}`);
370 }
371 res.join('&')
372 "#,
373 )
374 .catch(&ctx)
375 .unwrap();
376 assert_eq!(result, "a=1&b=2&a=3");
377 })
378 }
379
380 #[test]
381 fn test_iterate_entries() {
382 test_with(|ctx| {
383 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
384 let result = ctx
385 .eval::<String, _>(
386 r#"
387 const params = new URLSearchParams();
388 params.append('a', '1');
389 params.append('b', '2');
390 params.append('a', '3');
391 let res = [];
392 for (const [name, value] of params.entries()) {
393 res.push(`${name}=${value}`);
394 }
395 res.join('&')
396 "#,
397 )
398 .catch(&ctx)
399 .unwrap();
400 assert_eq!(result, "a=1&b=2&a=3");
401 })
402 }
403
404 #[test]
405 fn test_iterate_keys() {
406 test_with(|ctx| {
407 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
408 let result = ctx
409 .eval::<String, _>(
410 r#"
411 const params = new URLSearchParams();
412 params.append('a', '1');
413 params.append('b', '2');
414 params.append('a', '3');
415 let res = [];
416 for (const name of params.keys()) {
417 res.push(name);
418 }
419 res.join('&')
420 "#,
421 )
422 .catch(&ctx)
423 .unwrap();
424 assert_eq!(result, "a&b&a");
425 })
426 }
427
428 #[test]
429 fn test_iterate_values() {
430 test_with(|ctx| {
431 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
432 let result = ctx
433 .eval::<String, _>(
434 r#"
435 const params = new URLSearchParams();
436 params.append('a', '1');
437 params.append('b', '2');
438 params.append('a', '3');
439 let res = [];
440 for (const name of params.values()) {
441 res.push(name);
442 }
443 res.join('&')
444 "#,
445 )
446 .catch(&ctx)
447 .unwrap();
448 assert_eq!(result, "1&2&3");
449 })
450 }
451
452 #[test]
453 fn test_new_string() {
454 test_with(|ctx| {
455 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
456 let result = ctx
457 .eval::<String, _>(
458 r#"
459 const params = new URLSearchParams('a=1&b=2&a=3');
460 params.toString()
461 "#,
462 )
463 .catch(&ctx)
464 .unwrap();
465 assert_eq!(result, "a=1&b=2&a=3");
466 })
467 }
468
469 #[test]
470 fn test_new_string_url() {
471 test_with(|ctx| {
472 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
473 let result = ctx
474 .eval::<String, _>(
475 r#"
476 const params = new URLSearchParams('https://google.com?a=1&b=2&a=3');
477 params.toString()
478 "#,
479 )
480 .catch(&ctx)
481 .unwrap();
482 assert_eq!(result, "a=1&b=2&a=3");
483 })
484 }
485
486 #[test]
487 fn test_new_object() {
488 test_with(|ctx| {
489 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
490 let result = ctx
491 .eval::<String, _>(
492 r#"
493 const params = new URLSearchParams({'a': 1, 'b': 2});
494 params.toString()
495 "#,
496 )
497 .catch(&ctx)
498 .unwrap();
499 assert_eq!(result, "a=1&b=2");
500 })
501 }
502
503 #[test]
504 fn test_new_array() {
505 test_with(|ctx| {
506 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
507 let result = ctx
508 .eval::<String, _>(
509 r#"
510 const params = new URLSearchParams([['a', 1], ['b', 2], ['a', 3]]);
511 params.toString()
512 "#,
513 )
514 .catch(&ctx)
515 .unwrap();
516 assert_eq!(result, "a=1&b=2&a=3");
517 })
518 }
519
520 #[test]
521 fn test_new_iterator() {
522 test_with(|ctx| {
523 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
524 let result = ctx
525 .eval::<String, _>(
526 r#"
527 const params = new URLSearchParams();
528 params.append('a', '1');
529 params.append('b', '2');
530 params.append('a', '3');
531 const params2 = new URLSearchParams(params.entries());
532 params2.toString()
533 "#,
534 )
535 .catch(&ctx)
536 .unwrap();
537 assert_eq!(result, "a=1&b=2&a=3");
538 })
539 }
540
541 #[test]
542 fn test_size() {
543 test_with(|ctx| {
544 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
545 let result = ctx
546 .eval::<usize, _>(
547 r#"
548 const params = new URLSearchParams();
549 params.append('a', '1');
550 params.append('b', '2');
551 params.append('a', '3');
552 params.size
553 "#,
554 )
555 .catch(&ctx)
556 .unwrap();
557 assert_eq!(result, 3);
558 })
559 }
560
561 #[test]
562 fn test_set() {
563 test_with(|ctx| {
564 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
565 let result = ctx
566 .eval::<String, _>(
567 r#"
568 const params = new URLSearchParams();
569 params.append('a', '1');
570 params.append('b', '2');
571 params.append('a', '3');
572 params.set('a', '4');
573 params.toString()
574 "#,
575 )
576 .catch(&ctx)
577 .unwrap();
578 assert_eq!(result, "a=4&b=2");
579 })
580 }
581
582 #[test]
583 fn test_get() {
584 test_with(|ctx| {
585 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
586 let result = ctx
587 .eval::<String, _>(
588 r#"
589 const params = new URLSearchParams();
590 params.append('a', '1');
591 params.append('b', '2');
592 params.append('a', '3');
593 params.get('a')
594 "#,
595 )
596 .catch(&ctx)
597 .unwrap();
598 assert_eq!(result, "1");
599 })
600 }
601
602 #[test]
603 fn test_get_missing() {
604 test_with(|ctx| {
605 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
606 let result = ctx
607 .eval::<bool, _>(
608 r#"
609 const params = new URLSearchParams();
610 params.append('a', '1');
611 params.append('b', '2');
612 params.append('a', '3');
613 params.get('c') === null
614 "#,
615 )
616 .catch(&ctx)
617 .unwrap();
618 assert!(result);
619 })
620 }
621
622 #[test]
623 fn test_get_all() {
624 test_with(|ctx| {
625 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
626 let result = ctx
627 .eval::<String, _>(
628 r#"
629 const params = new URLSearchParams();
630 params.append('a', '1');
631 params.append('b', '2');
632 params.append('a', '3');
633 params.getAll('a').join('&')
634 "#,
635 )
636 .catch(&ctx)
637 .unwrap();
638 assert_eq!(result, "1&3");
639 })
640 }
641
642 #[test]
643 fn test_get_all_missing() {
644 test_with(|ctx| {
645 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
646 let result = ctx
647 .eval::<String, _>(
648 r#"
649 const params = new URLSearchParams();
650 params.append('a', '1');
651 params.append('b', '2');
652 params.append('a', '3');
653 params.getAll('c').join('&')
654 "#,
655 )
656 .catch(&ctx)
657 .unwrap();
658 assert_eq!(result, "");
659 })
660 }
661
662 #[test]
663 fn test_has() {
664 test_with(|ctx| {
665 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
666 let result = ctx
667 .eval::<bool, _>(
668 r#"
669 const params = new URLSearchParams();
670 params.append('a', '1');
671 params.append('b', '2');
672 params.append('a', '3');
673 params.has('b')
674 "#,
675 )
676 .catch(&ctx)
677 .unwrap();
678 assert!(result);
679 })
680 }
681
682 #[test]
683 fn test_has_value() {
684 test_with(|ctx| {
685 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
686 let result = ctx
687 .eval::<bool, _>(
688 r#"
689 const params = new URLSearchParams();
690 params.append('a', '1');
691 params.append('b', '2');
692 params.append('a', '3');
693 params.has('b', 5)
694 "#,
695 )
696 .catch(&ctx)
697 .unwrap();
698 assert!(!result);
699 })
700 }
701
702 #[test]
703 fn test_has_not() {
704 test_with(|ctx| {
705 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
706 let result = ctx
707 .eval::<bool, _>(
708 r#"
709 const params = new URLSearchParams();
710 params.append('a', '1');
711 params.append('b', '2');
712 params.append('a', '3');
713 params.has('c')
714 "#,
715 )
716 .catch(&ctx)
717 .unwrap();
718 assert!(!result);
719 })
720 }
721
722 #[test]
723 fn test_sort() {
724 test_with(|ctx| {
725 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
726 let result = ctx
727 .eval::<String, _>(
728 r#"
729 const params = new URLSearchParams();
730 params.append('a', '3');
731 params.append('b', '2');
732 params.append('a', '1');
733 params.sort();
734 params.toString()
735 "#,
736 )
737 .catch(&ctx)
738 .unwrap();
739 assert_eq!(result, "a=3&a=1&b=2");
740 })
741 }
742
743 #[test]
744 fn test_for_each() {
745 test_with(|ctx| {
746 Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
747 let result = ctx
748 .eval::<String, _>(
749 r#"
750 const params = new URLSearchParams();
751 params.append('a', '3');
752 params.append('b', '2');
753 params.append('a', '1');
754 let res = [];
755 params.forEach((name, value) => {
756 res.push(`${name}=${value}`);
757 });
758 res.join('&')
759 "#,
760 )
761 .catch(&ctx)
762 .unwrap();
763 assert_eq!(result, "a=3&b=2&a=1");
764 })
765 }
766}