rquickjs_extra_url/
url_search_params.rs

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/// The URLSearchParams interface defines utility methods to work with the query string of a URL.
10#[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    /// Returns a URLSearchParams object instance.
19    #[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    /// Returns an iterator allowing iteration through all key/value pairs contained in this object in the same order as they appear in the query string.
90    #[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    /// Appends a specified key/value pair as a new search parameter.
96    fn append(&mut self, name: Coerced<String>, value: Coerced<String>) {
97        self.data.push((name.0, value.0));
98    }
99
100    /// Deletes search parameters that match a name, and optional value, from the list of all search parameters.
101    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    /// Returns an iterator allowing iteration through all key/value pairs contained in this object in the same order as they appear in the query string.
116    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    /// Allows iteration through all values contained in this object via a callback function.
151    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    /// Returns the first value associated with the given search parameter.
163    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    /// Returns all the values associated with a given search parameter.
175    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    /// Returns a boolean value indicating if a given parameter, or parameter and value pair, exists.
190    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    /// Returns an iterator allowing iteration through all keys of the key/value pairs contained in this object.
205    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    /// Indicates the total number of search parameter entries.
234    #[qjs(get)]
235    fn size(&self) -> usize {
236        self.data.len()
237    }
238
239    /// Sets the value associated with a given search parameter to the given value. If there are several values, the others are deleted.
240    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    /// Sorts all key/value pairs, if any, by their keys.
258    fn sort(&mut self) {
259        self.data.sort_by(|(a, _), (b, _)| a.cmp(b));
260    }
261
262    /// Returns a string containing a query string suitable for use in a URL.
263    #[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    /// Returns an iterator allowing iteration through all values of the key/value pairs contained in this object.
273    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}