ploidy_util/
query.rs

1//! OpenAPI query parameter serialization.
2//!
3//! This module provides Serde-based serialization for OpenAPI 3.x
4//! query parameters, and supports all standard query styles:
5//! `form`, `deepObject`, `spaceDelimited`, and `pipeDelimited`.
6//!
7//! # Examples
8//!
9//! ```
10//! use url::Url;
11//! use ploidy_util::query::{QuerySerializer, QueryStyle};
12//! # use ploidy_util::query::QueryParamError;
13//!
14//! # fn main() -> Result<(), QueryParamError> {
15//! # use serde::Serialize;
16//! #[derive(Serialize)]
17//! #[serde(rename_all = "lowercase")]
18//! enum Kind {
19//!     Dog,
20//!     Cat,
21//!     Fish,
22//!     Bunny,
23//! }
24//!
25//! // Serialize parameters with the default style: `form`, exploded.
26//! let mut url = Url::parse("https://api.example.com/pets").unwrap();
27//! QuerySerializer::new(&mut url)
28//!     .append("kind", &[Kind::Dog, Kind::Cat])?
29//!     .append("limit", &10)?;
30//! assert_eq!(url.as_str(), "https://api.example.com/pets?kind=dog&kind=cat&limit=10");
31//!
32//! // ...Or as comma-separated values:
33//! let mut url = Url::parse("https://api.example.com/pets").unwrap();
34//! QuerySerializer::new(&mut url)
35//!     .style(QueryStyle::Form { exploded: false })
36//!     .append("kind", &[Kind::Dog, Kind::Cat])?;
37//! assert_eq!(url.as_str(), "https://api.example.com/pets?kind=dog,cat");
38//!
39//! // ...Or use `spaceDelimited` values:
40//! let mut url = Url::parse("https://api.example.com/pets").unwrap();
41//! QuerySerializer::new(&mut url)
42//!     .style(QueryStyle::SpaceDelimited)
43//!     .append("kind", &[Kind::Dog, Kind::Cat])?;
44//! assert_eq!(url.as_str(), "https://api.example.com/pets?kind=dog%20cat");
45//!
46//! // ...Or `pipeDelimited` values:
47//! let mut url = Url::parse("https://api.example.com/pets").unwrap();
48//! QuerySerializer::new(&mut url)
49//!     .style(QueryStyle::PipeDelimited)
50//!     .append("kind", &[Kind::Dog, Kind::Cat])?;
51//! assert_eq!(url.as_str(), "https://api.example.com/pets?kind=dog%7Ccat");
52//!
53//! // ...Or `deepObject` for nested structures:
54//! #[derive(Serialize)]
55//! struct Filter {
56//!     kind: Vec<Kind>,
57//!     term: String,
58//!     max_price: u32,
59//! }
60//!
61//! let filter = Filter {
62//!     kind: vec![Kind::Dog, Kind::Cat, Kind::Bunny],
63//!     term: "chow".to_owned(),
64//!     max_price: 30,
65//! };
66//!
67//! let mut url = Url::parse("https://api.example.com/search").unwrap();
68//! QuerySerializer::new(&mut url)
69//!     .style(QueryStyle::DeepObject)
70//!     .append("filter", &filter)?;
71//! assert!(url.query_pairs().eq([
72//!     ("filter[kind][0]".into(), "dog".into()),
73//!     ("filter[kind][1]".into(), "cat".into()),
74//!     ("filter[kind][2]".into(), "bunny".into()),
75//!     ("filter[term]".into(), "chow".into()),
76//!     ("filter[max_price]".into(), "30".into()),
77//! ]));
78//! # Ok(())
79//! # }
80//! ```
81
82use std::{borrow::Cow, fmt::Display};
83
84use itertools::Itertools;
85use percent_encoding::{AsciiSet, CONTROLS, PercentEncode};
86use serde::{
87    Serialize,
88    ser::{Impossible, SerializeMap, SerializeSeq, SerializeStruct, SerializeTuple},
89};
90use url::Url;
91
92/// Styles that describe how to format URL query parameters.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum QueryStyle {
95    /// Multiple values formatted as repeated parameters (if exploded),
96    /// or comma-separated values (if non-exploded).
97    ///
98    /// The exploded `form` style is the default.
99    Form { exploded: bool },
100
101    /// Multiple values separated by spaces.
102    SpaceDelimited,
103
104    /// Multiple values separated by pipes.
105    PipeDelimited,
106
107    /// Bracket notation for nested structures.
108    DeepObject,
109}
110
111impl Default for QueryStyle {
112    fn default() -> Self {
113        Self::Form { exploded: true }
114    }
115}
116
117/// A serializer that formats and appends URL query parameters
118/// according to OpenAPI styles.
119pub struct QuerySerializer<'a> {
120    url: &'a mut Url,
121    style: QueryStyle,
122}
123
124impl<'a> QuerySerializer<'a> {
125    /// Creates a new serializer.
126    pub fn new(url: &'a mut Url) -> Self {
127        Self {
128            url,
129            style: QueryStyle::default(),
130        }
131    }
132
133    /// Sets the formatting style.
134    pub fn style(mut self, style: QueryStyle) -> Self {
135        self.style = style;
136        self
137    }
138
139    /// Serializes and appends a query parameter to the URL.
140    pub fn append<T: Serialize>(self, name: &str, value: &T) -> Result<Self, QueryParamError> {
141        use ParamSerializerState::*;
142        let style = match self.style {
143            QueryStyle::DeepObject => DeepObject,
144            QueryStyle::Form { exploded: true } => ExplodedForm,
145            QueryStyle::Form { exploded: false } => NonExplodedForm(vec![]),
146            QueryStyle::PipeDelimited => Delimited("|", vec![]),
147            QueryStyle::SpaceDelimited => Delimited(" ", vec![]),
148        };
149        let mut path = KeyPath::new(name);
150        let mut serializer = QueryParamSerializer::new(self.url, &mut path, style);
151        value.serialize(&mut serializer)?;
152        serializer.flush();
153        Ok(self)
154    }
155}
156
157#[derive(Debug)]
158enum ParamSerializerState {
159    /// Non-exploded `spaceDelimited` or `pipeDelimited` style.
160    Delimited(&'static str, Vec<String>),
161    /// Exploded `form` style.
162    ExplodedForm,
163    /// Non-exploded `form` style.
164    NonExplodedForm(Vec<String>),
165    /// Exploded `deepObject` style.
166    DeepObject,
167}
168
169#[derive(Clone, Debug)]
170struct KeyPath<'a>(Cow<'a, str>, Vec<Cow<'a, str>>);
171
172impl<'a> KeyPath<'a> {
173    fn new(head: impl Into<Cow<'a, str>>) -> Self {
174        Self(head.into(), vec![])
175    }
176
177    fn len(&self) -> usize {
178        self.1.len().strict_add(1)
179    }
180
181    fn push(&mut self, segment: impl Into<Cow<'a, str>>) {
182        self.1.push(segment.into());
183    }
184
185    fn pop(&mut self) -> Cow<'a, str> {
186        self.1.pop().unwrap_or_else(|| self.0.clone())
187    }
188
189    fn first(&self) -> &str {
190        &self.0
191    }
192
193    fn last(&self) -> &str {
194        self.1.last().unwrap_or(&self.0)
195    }
196
197    fn split_first(&self) -> (&str, &[Cow<'a, str>]) {
198        (&self.0, &self.1)
199    }
200}
201
202/// The [component percent-encode set][component], as defined by
203/// the WHATWG URL Standard.
204///
205/// This is the [userinfo percent-encode set][userinfo] and
206/// U+0024 (`$`) to U+0026 (`&`), inclusive; U+002B (`+`); and U+002C (`,`).
207/// It gives identical results to JavaScript's `encodeURIComponent()`
208/// function.
209///
210/// [component]: https://url.spec.whatwg.org/#component-percent-encode-set
211/// [userinfo]: https://url.spec.whatwg.org/#userinfo-percent-encode-set
212const COMPONENT: &AsciiSet = &CONTROLS
213    .add(b' ')
214    .add(b'"')
215    .add(b'#')
216    .add(b'<')
217    .add(b'>')
218    .add(b'?')
219    .add(b'`')
220    .add(b'^')
221    .add(b'{')
222    .add(b'}')
223    .add(b'/')
224    .add(b':')
225    .add(b';')
226    .add(b'=')
227    .add(b'@')
228    .add(b'[')
229    .add(b'\\')
230    .add(b']')
231    .add(b'|')
232    .add(b'$')
233    .add(b'%')
234    .add(b'&')
235    .add(b'+')
236    .add(b',');
237
238#[derive(Clone, Debug)]
239enum EncodedOrRaw<'a> {
240    Encoded(PercentEncode<'a>),
241    Raw(&'a str),
242}
243
244impl<'a> EncodedOrRaw<'a> {
245    fn encode(input: &'a str) -> Self {
246        Self::Encoded(percent_encoding::utf8_percent_encode(input, COMPONENT))
247    }
248}
249
250impl Display for EncodedOrRaw<'_> {
251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        match self {
253            Self::Encoded(s) => write!(f, "{s}"),
254            Self::Raw(s) => f.write_str(s),
255        }
256    }
257}
258
259#[derive(Debug)]
260struct PercentEncodeDelimited<'a, T>(&'a [T], EncodedOrRaw<'a>);
261
262impl<T: AsRef<str>> Display for PercentEncodeDelimited<'_, T> {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        write!(
265            f,
266            "{}",
267            // Use the fully qualified syntax as suggested by
268            // rust-lang/rust#79524.
269            Itertools::intersperse(
270                self.0
271                    .iter()
272                    .map(|input| input.as_ref())
273                    .map(EncodedOrRaw::encode),
274                self.1.clone()
275            )
276            .format("")
277        )
278    }
279}
280
281/// A [`Serializer`][serde::Serializer] for a single query parameter.
282#[derive(Debug)]
283struct QueryParamSerializer<'a> {
284    /// A mutable reference to the URL being constructed.
285    url: &'a mut url::Url,
286    /// The current key path, starting with the parameter name.
287    /// The serializer pushes and pops additional segments for
288    /// nested structures.
289    path: &'a mut KeyPath<'a>,
290    state: ParamSerializerState,
291}
292
293impl<'a> QueryParamSerializer<'a> {
294    /// Creates a new query parameter serializer.
295    fn new(url: &'a mut url::Url, path: &'a mut KeyPath<'a>, state: ParamSerializerState) -> Self {
296        Self { url, path, state }
297    }
298
299    /// Computes the key for the current value, accounting for nesting.
300    fn key(&self) -> Cow<'_, str> {
301        use ParamSerializerState::*;
302        match &self.state {
303            DeepObject => {
304                // `deepObject` style uses `base[field1][field2]...`.
305                match self.path.split_first() {
306                    (head, []) => head.into(),
307                    (head, rest) => format!("{head}[{}]", rest.iter().format("][")).into(),
308                }
309            }
310            ExplodedForm => {
311                // Exploded `form` style uses the field name directly.
312                self.path.last().into()
313            }
314            NonExplodedForm(_) | Delimited(_, _) => {
315                // Non-exploded styles use the base parameter name directly.
316                self.path.first().into()
317            }
318        }
319    }
320
321    /// Appends an unencoded value, either to the buffer or directly to the URL.
322    fn append<'b>(&mut self, value: impl Into<Cow<'b, str>>) {
323        use ParamSerializerState::*;
324        let value = value.into();
325        match &mut self.state {
326            NonExplodedForm(buf) | Delimited(_, buf) => {
327                buf.push(value.into_owned());
328            }
329            DeepObject | ExplodedForm => {
330                // For exploded styles, append the key and value directly to the URL.
331                // This encodes them using `form-urlencoded` rules, not percent-encoding;
332                // OpenAPI allows both here.
333                let key = self.key().into_owned();
334                self.url.query_pairs_mut().append_pair(&key, &value);
335            }
336        }
337    }
338
339    /// Flushes any buffered values to the URL.
340    ///
341    /// This is called by compound serializers when they finish collecting values,
342    /// and by [`Serializer::append`] to write top-level values.
343    fn flush(&mut self) {
344        use ParamSerializerState::*;
345        let (delimiter, buf) = match &mut self.state {
346            NonExplodedForm(buf) => (
347                // For the non-exploded `form` style, commas aren't encoded.
348                EncodedOrRaw::Raw(","),
349                std::mem::take(buf),
350            ),
351            Delimited(delimiter, buf) => (
352                // For `spaceDelimited` and `pipeDelimited`, delimeters are encoded.
353                EncodedOrRaw::encode(delimiter),
354                std::mem::take(buf),
355            ),
356            _ => return,
357        };
358        if buf.is_empty() {
359            return;
360        }
361
362        let key = self.key();
363        let key = EncodedOrRaw::encode(&key);
364        let value = PercentEncodeDelimited(&buf, delimiter);
365
366        // Append the percent-encoded key and value to the existing query string.
367        // We avoid `query_pairs_mut()` here, because it uses `form-urlencoded` rules,
368        // while OpenAPI requires percent-encoding for "non-RFC6570 query string styles".
369        let new_query = match self.url.query().map(|q| q.trim_end_matches('&')) {
370            Some(query) if !query.is_empty() => format!("{query}&{key}={value}"),
371            _ => format!("{key}={value}"),
372        };
373        self.url.set_query(Some(&new_query));
374    }
375}
376
377impl<'a, 'b> serde::Serializer for &'a mut QueryParamSerializer<'b> {
378    type Ok = ();
379    type Error = QueryParamError;
380
381    type SerializeSeq = QuerySeqSerializer<'a, 'b>;
382    type SerializeTuple = QuerySeqSerializer<'a, 'b>;
383    type SerializeTupleStruct = Impossible<(), QueryParamError>;
384    type SerializeTupleVariant = Impossible<(), QueryParamError>;
385    type SerializeMap = QueryStructSerializer<'a, 'b>;
386    type SerializeStruct = QueryStructSerializer<'a, 'b>;
387    type SerializeStructVariant = Impossible<(), QueryParamError>;
388
389    fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error> {
390        self.append(if v { "true" } else { "false" });
391        Ok(())
392    }
393
394    fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error> {
395        self.append(v.to_string());
396        Ok(())
397    }
398
399    fn serialize_i16(self, v: i16) -> Result<Self::Ok, Self::Error> {
400        self.append(v.to_string());
401        Ok(())
402    }
403
404    fn serialize_i32(self, v: i32) -> Result<Self::Ok, Self::Error> {
405        self.append(v.to_string());
406        Ok(())
407    }
408
409    fn serialize_i64(self, v: i64) -> Result<Self::Ok, Self::Error> {
410        self.append(v.to_string());
411        Ok(())
412    }
413
414    fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error> {
415        self.append(v.to_string());
416        Ok(())
417    }
418
419    fn serialize_u16(self, v: u16) -> Result<Self::Ok, Self::Error> {
420        self.append(v.to_string());
421        Ok(())
422    }
423
424    fn serialize_u32(self, v: u32) -> Result<Self::Ok, Self::Error> {
425        self.append(v.to_string());
426        Ok(())
427    }
428
429    fn serialize_u64(self, v: u64) -> Result<Self::Ok, Self::Error> {
430        self.append(v.to_string());
431        Ok(())
432    }
433
434    fn serialize_f32(self, v: f32) -> Result<Self::Ok, Self::Error> {
435        self.append(v.to_string());
436        Ok(())
437    }
438
439    fn serialize_f64(self, v: f64) -> Result<Self::Ok, Self::Error> {
440        self.append(v.to_string());
441        Ok(())
442    }
443
444    fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error> {
445        self.append(v.to_string());
446        Ok(())
447    }
448
449    fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
450        self.append(v);
451        Ok(())
452    }
453
454    fn serialize_bytes(self, _v: &[u8]) -> Result<Self::Ok, Self::Error> {
455        Err(UnsupportedTypeError::Bytes)?
456    }
457
458    fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
459        // Don't emit query parameters for `None`.
460        Ok(())
461    }
462
463    fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<Self::Ok, Self::Error> {
464        value.serialize(self)
465    }
466
467    fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
468        Err(UnsupportedTypeError::Unit)?
469    }
470
471    fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok, Self::Error> {
472        Err(UnsupportedTypeError::UnitStruct(name))?
473    }
474
475    fn serialize_unit_variant(
476        self,
477        _name: &'static str,
478        _index: u32,
479        variant: &'static str,
480    ) -> Result<Self::Ok, Self::Error> {
481        self.append(variant);
482        Ok(())
483    }
484
485    fn serialize_newtype_struct<T: ?Sized + Serialize>(
486        self,
487        _name: &'static str,
488        value: &T,
489    ) -> Result<Self::Ok, Self::Error> {
490        value.serialize(self)
491    }
492
493    fn serialize_newtype_variant<T: ?Sized + Serialize>(
494        self,
495        name: &'static str,
496        _index: u32,
497        variant: &'static str,
498        _value: &T,
499    ) -> Result<Self::Ok, Self::Error> {
500        Err(UnsupportedTypeError::NewtypeVariant(name, variant))?
501    }
502
503    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
504        Ok(QuerySeqSerializer {
505            serializer: self,
506            index: 0,
507        })
508    }
509
510    fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
511        Ok(QuerySeqSerializer {
512            serializer: self,
513            index: 0,
514        })
515    }
516
517    fn serialize_tuple_struct(
518        self,
519        name: &'static str,
520        _len: usize,
521    ) -> Result<Self::SerializeTupleStruct, Self::Error> {
522        Err(UnsupportedTypeError::TupleStruct(name))?
523    }
524
525    fn serialize_tuple_variant(
526        self,
527        name: &'static str,
528        _index: u32,
529        variant: &'static str,
530        _len: usize,
531    ) -> Result<Self::SerializeTupleVariant, Self::Error> {
532        Err(UnsupportedTypeError::TupleVariant(name, variant))?
533    }
534
535    fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
536        Ok(QueryStructSerializer { serializer: self })
537    }
538
539    fn serialize_struct(
540        self,
541        _name: &'static str,
542        _len: usize,
543    ) -> Result<Self::SerializeStruct, Self::Error> {
544        Ok(QueryStructSerializer { serializer: self })
545    }
546
547    fn serialize_struct_variant(
548        self,
549        name: &'static str,
550        _index: u32,
551        variant: &'static str,
552        _len: usize,
553    ) -> Result<Self::SerializeStructVariant, Self::Error> {
554        Err(UnsupportedTypeError::StructVariant(name, variant))?
555    }
556}
557
558/// A serializer for sequences (arrays) and tuples.
559pub struct QuerySeqSerializer<'a, 'b> {
560    serializer: &'a mut QueryParamSerializer<'b>,
561    index: usize,
562}
563
564impl<'a, 'b> SerializeSeq for QuerySeqSerializer<'a, 'b> {
565    type Ok = ();
566    type Error = QueryParamError;
567
568    fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
569        use ParamSerializerState::*;
570        match &mut self.serializer.state {
571            DeepObject if self.serializer.path.len() == 1 => {
572                // OpenAPI doesn't define `deepObject` for top-level arrays; and
573                // we know we're at the top level if the key path has just one segment.
574                return Err(QueryParamError::UnspecifiedStyleExploded);
575            }
576            DeepObject => {
577                // Otherwise, we're inside a nested structure.
578                self.serializer.path.push(self.index.to_string());
579                value.serialize(&mut *self.serializer)?;
580                self.serializer.path.pop();
581            }
582            _ => value.serialize(&mut *self.serializer)?,
583        }
584        self.index += 1;
585        Ok(())
586    }
587
588    fn end(self) -> Result<Self::Ok, Self::Error> {
589        self.serializer.flush();
590        Ok(())
591    }
592}
593
594impl<'a, 'b> SerializeTuple for QuerySeqSerializer<'a, 'b> {
595    type Ok = ();
596    type Error = QueryParamError;
597
598    fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
599        SerializeSeq::serialize_element(self, value)
600    }
601
602    fn end(self) -> Result<Self::Ok, Self::Error> {
603        SerializeSeq::end(self)
604    }
605}
606
607/// A serializer for structs and maps (objects).
608pub struct QueryStructSerializer<'a, 'b> {
609    serializer: &'a mut QueryParamSerializer<'b>,
610}
611
612impl<'a, 'b> SerializeStruct for QueryStructSerializer<'a, 'b> {
613    type Ok = ();
614    type Error = QueryParamError;
615
616    fn serialize_field<T: ?Sized + Serialize>(
617        &mut self,
618        key: &'static str,
619        value: &T,
620    ) -> Result<(), Self::Error> {
621        use ParamSerializerState::*;
622        if let NonExplodedForm(buf) | Delimited(_, buf) = &mut self.serializer.state {
623            // For non-exploded styles, insert the key before the value.
624            // This creates alternating key-value pairs.
625            buf.push(key.to_owned());
626        };
627
628        self.serializer.path.push(key);
629        value.serialize(&mut *self.serializer)?;
630        self.serializer.path.pop();
631        Ok(())
632    }
633
634    fn end(self) -> Result<Self::Ok, Self::Error> {
635        self.serializer.flush();
636        Ok(())
637    }
638}
639
640impl<'a, 'b> SerializeMap for QueryStructSerializer<'a, 'b> {
641    type Ok = ();
642    type Error = QueryParamError;
643
644    fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<(), Self::Error> {
645        let mut extractor = KeyExtractor { key: String::new() };
646        key.serialize(&mut extractor)?;
647        self.serializer.path.push(extractor.key);
648        Ok(())
649    }
650
651    fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
652        use ParamSerializerState::*;
653        if let NonExplodedForm(buf) | Delimited(_, buf) = &mut self.serializer.state {
654            // For non-exploded styles, insert the key before the value
655            // (`serialize_key()` already added the key to the path).
656            buf.push(self.serializer.path.last().to_owned());
657        };
658
659        value.serialize(&mut *self.serializer)?;
660        self.serializer.path.pop();
661        Ok(())
662    }
663
664    fn end(self) -> Result<Self::Ok, Self::Error> {
665        SerializeStruct::end(self)
666    }
667}
668
669/// A helper [`Serializer`][serde::Serializer] for extracting string keys
670/// from maps.
671struct KeyExtractor {
672    key: String,
673}
674
675impl serde::Serializer for &mut KeyExtractor {
676    type Ok = ();
677    type Error = QueryParamError;
678
679    type SerializeSeq = Impossible<(), QueryParamError>;
680    type SerializeTuple = Impossible<(), QueryParamError>;
681    type SerializeTupleStruct = Impossible<(), QueryParamError>;
682    type SerializeTupleVariant = Impossible<(), QueryParamError>;
683    type SerializeMap = Impossible<(), QueryParamError>;
684    type SerializeStruct = Impossible<(), QueryParamError>;
685    type SerializeStructVariant = Impossible<(), QueryParamError>;
686
687    fn serialize_bool(self, _: bool) -> Result<Self::Ok, Self::Error> {
688        Err(QueryParamError::MapKeyNotString)
689    }
690
691    fn serialize_i8(self, _: i8) -> Result<Self::Ok, Self::Error> {
692        Err(QueryParamError::MapKeyNotString)
693    }
694
695    fn serialize_i16(self, _: i16) -> Result<Self::Ok, Self::Error> {
696        Err(QueryParamError::MapKeyNotString)
697    }
698
699    fn serialize_i32(self, _: i32) -> Result<Self::Ok, Self::Error> {
700        Err(QueryParamError::MapKeyNotString)
701    }
702
703    fn serialize_i64(self, _: i64) -> Result<Self::Ok, Self::Error> {
704        Err(QueryParamError::MapKeyNotString)
705    }
706
707    fn serialize_u8(self, _: u8) -> Result<Self::Ok, Self::Error> {
708        Err(QueryParamError::MapKeyNotString)
709    }
710
711    fn serialize_u16(self, _: u16) -> Result<Self::Ok, Self::Error> {
712        Err(QueryParamError::MapKeyNotString)
713    }
714
715    fn serialize_u32(self, _: u32) -> Result<Self::Ok, Self::Error> {
716        Err(QueryParamError::MapKeyNotString)
717    }
718
719    fn serialize_u64(self, _: u64) -> Result<Self::Ok, Self::Error> {
720        Err(QueryParamError::MapKeyNotString)
721    }
722
723    fn serialize_f32(self, _: f32) -> Result<Self::Ok, Self::Error> {
724        Err(QueryParamError::MapKeyNotString)
725    }
726
727    fn serialize_f64(self, _: f64) -> Result<Self::Ok, Self::Error> {
728        Err(QueryParamError::MapKeyNotString)
729    }
730
731    fn serialize_char(self, _: char) -> Result<Self::Ok, Self::Error> {
732        Err(QueryParamError::MapKeyNotString)
733    }
734
735    fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
736        self.key = v.to_owned();
737        Ok(())
738    }
739
740    fn serialize_bytes(self, _: &[u8]) -> Result<Self::Ok, Self::Error> {
741        Err(QueryParamError::MapKeyNotString)
742    }
743
744    fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
745        Err(QueryParamError::MapKeyNotString)
746    }
747
748    fn serialize_some<T: ?Sized + Serialize>(self, _: &T) -> Result<Self::Ok, Self::Error> {
749        Err(QueryParamError::MapKeyNotString)
750    }
751
752    fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
753        Err(QueryParamError::MapKeyNotString)
754    }
755
756    fn serialize_unit_struct(self, _: &'static str) -> Result<Self::Ok, Self::Error> {
757        Err(QueryParamError::MapKeyNotString)
758    }
759
760    fn serialize_unit_variant(
761        self,
762        _: &'static str,
763        _: u32,
764        _: &'static str,
765    ) -> Result<Self::Ok, Self::Error> {
766        Err(QueryParamError::MapKeyNotString)
767    }
768
769    fn serialize_newtype_struct<T: ?Sized + Serialize>(
770        self,
771        _: &'static str,
772        _: &T,
773    ) -> Result<Self::Ok, Self::Error> {
774        Err(QueryParamError::MapKeyNotString)
775    }
776
777    fn serialize_newtype_variant<T: ?Sized + Serialize>(
778        self,
779        _: &'static str,
780        _: u32,
781        _: &'static str,
782        _: &T,
783    ) -> Result<Self::Ok, Self::Error> {
784        Err(QueryParamError::MapKeyNotString)
785    }
786
787    fn serialize_seq(self, _: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
788        Err(QueryParamError::MapKeyNotString)
789    }
790
791    fn serialize_tuple(self, _: usize) -> Result<Self::SerializeTuple, Self::Error> {
792        Err(QueryParamError::MapKeyNotString)
793    }
794
795    fn serialize_tuple_struct(
796        self,
797        _: &'static str,
798        _: usize,
799    ) -> Result<Self::SerializeTupleStruct, Self::Error> {
800        Err(QueryParamError::MapKeyNotString)
801    }
802
803    fn serialize_tuple_variant(
804        self,
805        _: &'static str,
806        _: u32,
807        _: &'static str,
808        _: usize,
809    ) -> Result<Self::SerializeTupleVariant, Self::Error> {
810        Err(QueryParamError::MapKeyNotString)
811    }
812
813    fn serialize_map(self, _: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
814        Err(QueryParamError::MapKeyNotString)
815    }
816
817    fn serialize_struct(
818        self,
819        _: &'static str,
820        _: usize,
821    ) -> Result<Self::SerializeStruct, Self::Error> {
822        Err(QueryParamError::MapKeyNotString)
823    }
824
825    fn serialize_struct_variant(
826        self,
827        _: &'static str,
828        _: u32,
829        _: &'static str,
830        _: usize,
831    ) -> Result<Self::SerializeStructVariant, Self::Error> {
832        Err(QueryParamError::MapKeyNotString)
833    }
834}
835
836/// An error that occurs during query parameter serialization.
837#[derive(Debug, thiserror::Error)]
838pub enum QueryParamError {
839    #[error("can't serialize {0} as query parameter")]
840    UnsupportedType(#[from] UnsupportedTypeError),
841    #[error("style-exploded combination not defined by OpenAPI")]
842    UnspecifiedStyleExploded,
843    #[error("map keys must be strings")]
844    MapKeyNotString,
845    #[error("{0}")]
846    Custom(String),
847}
848
849impl serde::ser::Error for QueryParamError {
850    fn custom<T: std::fmt::Display>(err: T) -> Self {
851        Self::Custom(err.to_string())
852    }
853}
854
855#[derive(Debug, thiserror::Error)]
856pub enum UnsupportedTypeError {
857    #[error("bytes")]
858    Bytes,
859    #[error("unit")]
860    Unit,
861    #[error("unit struct `{0}`")]
862    UnitStruct(&'static str),
863    #[error("tuple struct `{0}`")]
864    TupleStruct(&'static str),
865    #[error("newtype variant `{1}` of `{0}`")]
866    NewtypeVariant(&'static str, &'static str),
867    #[error("tuple variant `{1}` of `{0}`")]
868    TupleVariant(&'static str, &'static str),
869    #[error("struct variant `{1}` of `{0}`")]
870    StructVariant(&'static str, &'static str),
871}
872
873#[cfg(test)]
874mod tests {
875    use super::*;
876    use serde::Serialize;
877    use url::Url;
878
879    #[test]
880    fn test_integer() {
881        let mut url = Url::parse("http://example.com/").unwrap();
882        QuerySerializer::new(&mut url).append("limit", &42).unwrap();
883        assert_eq!(url.query(), Some("limit=42"));
884    }
885
886    #[test]
887    fn test_string() {
888        let mut url = Url::parse("http://example.com/").unwrap();
889        QuerySerializer::new(&mut url)
890            .append("name", &"Alice")
891            .unwrap();
892        assert_eq!(url.query(), Some("name=Alice"));
893    }
894
895    #[test]
896    fn test_bool() {
897        let mut url = Url::parse("http://example.com/").unwrap();
898        QuerySerializer::new(&mut url)
899            .append("active", &true)
900            .unwrap();
901        assert_eq!(url.query(), Some("active=true"));
902    }
903
904    #[test]
905    fn test_option_some() {
906        let mut url = Url::parse("http://example.com/").unwrap();
907        let value = Some(42);
908        QuerySerializer::new(&mut url)
909            .append("limit", &value)
910            .unwrap();
911        assert_eq!(url.query(), Some("limit=42"));
912    }
913
914    #[test]
915    fn test_option_none() {
916        let mut url = Url::parse("http://example.com/").unwrap();
917        let value: Option<i32> = None;
918        QuerySerializer::new(&mut url)
919            .append("limit", &value)
920            .unwrap();
921        assert_eq!(url.query(), None);
922    }
923
924    #[test]
925    fn test_array_form_exploded() {
926        let mut url = Url::parse("http://example.com/").unwrap();
927        let values = vec![1, 2, 3];
928        QuerySerializer::new(&mut url)
929            .append("ids", &values)
930            .unwrap();
931        assert_eq!(url.query(), Some("ids=1&ids=2&ids=3"));
932    }
933
934    #[test]
935    fn test_array_form_non_exploded() {
936        let mut url = Url::parse("http://example.com/").unwrap();
937        let values = vec![1, 2, 3];
938        QuerySerializer::new(&mut url)
939            .style(QueryStyle::Form { exploded: false })
940            .append("ids", &values)
941            .unwrap();
942        assert_eq!(url.query(), Some("ids=1,2,3"));
943    }
944
945    #[test]
946    fn test_array_space_delimited() {
947        let mut url = Url::parse("http://example.com/").unwrap();
948        let values = vec![1, 2, 3];
949        QuerySerializer::new(&mut url)
950            .style(QueryStyle::SpaceDelimited)
951            .append("ids", &values)
952            .unwrap();
953        assert_eq!(url.query(), Some("ids=1%202%203"));
954    }
955
956    #[test]
957    fn test_array_pipe_delimited() {
958        let mut url = Url::parse("http://example.com/").unwrap();
959        let values = vec![1, 2, 3];
960        QuerySerializer::new(&mut url)
961            .style(QueryStyle::PipeDelimited)
962            .append("ids", &values)
963            .unwrap();
964        assert_eq!(url.query(), Some("ids=1%7C2%7C3"));
965    }
966
967    #[test]
968    fn test_empty_array() {
969        let mut url = Url::parse("http://example.com/").unwrap();
970        let values: Vec<i32> = vec![];
971        QuerySerializer::new(&mut url)
972            .append("ids", &values)
973            .unwrap();
974        assert_eq!(url.query(), None);
975    }
976
977    #[test]
978    fn test_object_form_exploded() {
979        #[derive(Serialize)]
980        struct Person {
981            first_name: String,
982            last_name: String,
983        }
984
985        let mut url = Url::parse("http://example.com/").unwrap();
986        let person = Person {
987            first_name: "John".to_owned(),
988            last_name: "Doe".to_owned(),
989        };
990        QuerySerializer::new(&mut url)
991            .append("person", &person)
992            .unwrap();
993        assert_eq!(url.query(), Some("first_name=John&last_name=Doe"));
994    }
995
996    #[test]
997    fn test_object_form_non_exploded() {
998        #[derive(Serialize)]
999        struct Person {
1000            first_name: String,
1001            last_name: String,
1002        }
1003
1004        let mut url = Url::parse("http://example.com/").unwrap();
1005        let person = Person {
1006            first_name: "John".to_owned(),
1007            last_name: "Doe".to_owned(),
1008        };
1009        QuerySerializer::new(&mut url)
1010            .style(QueryStyle::Form { exploded: false })
1011            .append("person", &person)
1012            .unwrap();
1013        assert_eq!(url.query(), Some("person=first_name,John,last_name,Doe"));
1014    }
1015
1016    #[test]
1017    fn test_object_deep_object() {
1018        #[derive(Serialize)]
1019        struct Filter {
1020            #[serde(rename = "type")]
1021            type_field: String,
1022            location: String,
1023        }
1024
1025        let mut url = Url::parse("http://example.com/").unwrap();
1026        let filter = Filter {
1027            type_field: "cocktail".to_owned(),
1028            location: "bar".to_owned(),
1029        };
1030        QuerySerializer::new(&mut url)
1031            .style(QueryStyle::DeepObject)
1032            .append("filter", &filter)
1033            .unwrap();
1034        assert_eq!(
1035            url.query(),
1036            Some("filter%5Btype%5D=cocktail&filter%5Blocation%5D=bar")
1037        );
1038    }
1039
1040    #[test]
1041    fn test_multiple_params_chained() {
1042        let mut url = Url::parse("http://example.com/").unwrap();
1043        let tags = vec!["dog", "cat"];
1044        QuerySerializer::new(&mut url)
1045            .append("limit", &10)
1046            .unwrap()
1047            .append("tags", &tags)
1048            .unwrap();
1049        assert_eq!(url.query(), Some("limit=10&tags=dog&tags=cat"));
1050    }
1051
1052    #[test]
1053    fn test_string_with_special_chars() {
1054        let mut url = Url::parse("http://example.com/").unwrap();
1055        QuerySerializer::new(&mut url)
1056            .append("name", &"John Doe & Co.")
1057            .unwrap();
1058        assert_eq!(url.query(), Some("name=John+Doe+%26+Co."));
1059    }
1060
1061    #[test]
1062    fn test_array_of_strings_with_special_chars() {
1063        let mut url = Url::parse("http://example.com/").unwrap();
1064        let values = vec!["hello world", "foo&bar"];
1065        QuerySerializer::new(&mut url)
1066            .style(QueryStyle::Form { exploded: false })
1067            .append("tags", &values)
1068            .unwrap();
1069        assert_eq!(url.query(), Some("tags=hello%20world,foo%26bar"));
1070    }
1071
1072    #[test]
1073    fn test_nested_deep_object() {
1074        #[derive(Serialize)]
1075        struct Address {
1076            city: String,
1077            country: String,
1078        }
1079
1080        #[derive(Serialize)]
1081        struct Person {
1082            name: String,
1083            address: Address,
1084        }
1085
1086        let mut url = Url::parse("http://example.com/").unwrap();
1087        let person = Person {
1088            name: "Alice".to_owned(),
1089            address: Address {
1090                city: "Paris".to_owned(),
1091                country: "France".to_owned(),
1092            },
1093        };
1094        QuerySerializer::new(&mut url)
1095            .style(QueryStyle::DeepObject)
1096            .append("person", &person)
1097            .unwrap();
1098        assert_eq!(
1099            url.query(),
1100            Some(
1101                "person%5Bname%5D=Alice&person%5Baddress%5D%5Bcity%5D=Paris&person%5Baddress%5D%5Bcountry%5D=France"
1102            )
1103        );
1104    }
1105
1106    #[test]
1107    fn test_deep_object_with_array_field() {
1108        #[derive(Serialize)]
1109        struct Filter {
1110            category: String,
1111            tags: Vec<String>,
1112        }
1113
1114        let mut url = Url::parse("http://example.com/").unwrap();
1115        let filter = Filter {
1116            category: "electronics".to_owned(),
1117            tags: vec!["new".to_owned(), "sale".to_owned()],
1118        };
1119        QuerySerializer::new(&mut url)
1120            .style(QueryStyle::DeepObject)
1121            .append("filter", &filter)
1122            .unwrap();
1123        assert_eq!(
1124            url.query(),
1125            Some(
1126                "filter%5Bcategory%5D=electronics&filter%5Btags%5D%5B0%5D=new&filter%5Btags%5D%5B1%5D=sale"
1127            )
1128        );
1129    }
1130
1131    #[test]
1132    fn test_serde_skip_if() {
1133        #[derive(Serialize)]
1134        struct Params {
1135            required: i32,
1136            #[serde(skip_serializing_if = "Option::is_none")]
1137            optional: Option<String>,
1138        }
1139
1140        let mut url = Url::parse("http://example.com/").unwrap();
1141        let params = Params {
1142            required: 42,
1143            optional: None,
1144        };
1145        QuerySerializer::new(&mut url)
1146            .append("params", &params)
1147            .unwrap();
1148        assert_eq!(url.query(), Some("required=42"));
1149    }
1150
1151    #[test]
1152    fn test_unit_variant_enum() {
1153        #[derive(Serialize)]
1154        #[allow(dead_code)]
1155        enum Status {
1156            Active,
1157            Inactive,
1158        }
1159
1160        let mut url = Url::parse("http://example.com/").unwrap();
1161        QuerySerializer::new(&mut url)
1162            .append("status", &Status::Active)
1163            .unwrap();
1164        assert_eq!(url.query(), Some("status=Active"));
1165    }
1166
1167    #[test]
1168    fn test_unicode_string() {
1169        let mut url = Url::parse("http://example.com/").unwrap();
1170        QuerySerializer::new(&mut url)
1171            .append("name", &"日本語")
1172            .unwrap();
1173        assert_eq!(url.query(), Some("name=%E6%97%A5%E6%9C%AC%E8%AA%9E"));
1174    }
1175
1176    #[test]
1177    fn test_deep_object_rejects_arrays() {
1178        let mut url = Url::parse("http://example.com/").unwrap();
1179        let values = vec![1, 2, 3];
1180        let result = QuerySerializer::new(&mut url)
1181            .style(QueryStyle::DeepObject)
1182            .append("ids", &values);
1183        assert!(matches!(
1184            result,
1185            Err(QueryParamError::UnspecifiedStyleExploded)
1186        ));
1187    }
1188
1189    #[test]
1190    fn test_space_delimited_object() {
1191        #[derive(Serialize)]
1192        struct Color {
1193            r: u32,
1194            g: u32,
1195            b: u32,
1196        }
1197
1198        let mut url = Url::parse("http://example.com/").unwrap();
1199        let color = Color {
1200            r: 100,
1201            g: 200,
1202            b: 150,
1203        };
1204        QuerySerializer::new(&mut url)
1205            .style(QueryStyle::SpaceDelimited)
1206            .append("color", &color)
1207            .unwrap();
1208
1209        // Per OpenAPI spec: `color=R%20100%20G%20200%20B%20150`.
1210        assert_eq!(url.query(), Some("color=r%20100%20g%20200%20b%20150"));
1211    }
1212
1213    #[test]
1214    fn test_pipe_delimited_object() {
1215        #[derive(Serialize)]
1216        struct Color {
1217            r: u32,
1218            g: u32,
1219            b: u32,
1220        }
1221
1222        let mut url = Url::parse("http://example.com/").unwrap();
1223        let color = Color {
1224            r: 100,
1225            g: 200,
1226            b: 150,
1227        };
1228        QuerySerializer::new(&mut url)
1229            .style(QueryStyle::PipeDelimited)
1230            .append("color", &color)
1231            .unwrap();
1232
1233        // Per OpenAPI spec: `color=R%7C100%7CG%7C200%7CB%7C150`.
1234        assert_eq!(url.query(), Some("color=r%7C100%7Cg%7C200%7Cb%7C150"));
1235    }
1236
1237    #[test]
1238    fn test_tuple_form_exploded() {
1239        let mut url = Url::parse("http://example.com/").unwrap();
1240        let coords = (42, 24, 10);
1241        QuerySerializer::new(&mut url)
1242            .append("coords", &coords)
1243            .unwrap();
1244        assert_eq!(url.query(), Some("coords=42&coords=24&coords=10"));
1245    }
1246
1247    #[test]
1248    fn test_tuple_form_non_exploded() {
1249        let mut url = Url::parse("http://example.com/").unwrap();
1250        let coords = (42, 24, 10);
1251        QuerySerializer::new(&mut url)
1252            .style(QueryStyle::Form { exploded: false })
1253            .append("coords", &coords)
1254            .unwrap();
1255        assert_eq!(url.query(), Some("coords=42,24,10"));
1256    }
1257
1258    #[test]
1259    fn test_tuple_space_delimited() {
1260        let mut url = Url::parse("http://example.com/").unwrap();
1261        let coords = (42, 24, 10);
1262        QuerySerializer::new(&mut url)
1263            .style(QueryStyle::SpaceDelimited)
1264            .append("coords", &coords)
1265            .unwrap();
1266        assert_eq!(url.query(), Some("coords=42%2024%2010"));
1267    }
1268
1269    #[test]
1270    fn test_tuple_pipe_delimited() {
1271        let mut url = Url::parse("http://example.com/").unwrap();
1272        let coords = (42, 24, 10);
1273        QuerySerializer::new(&mut url)
1274            .style(QueryStyle::PipeDelimited)
1275            .append("coords", &coords)
1276            .unwrap();
1277        assert_eq!(url.query(), Some("coords=42%7C24%7C10"));
1278    }
1279}