Skip to main content

aws_smithy_query/codec/
serializer.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use aws_smithy_schema::codec::FinishSerializer;
7use aws_smithy_schema::extension::SchemaExtensionKey;
8use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeSerializer};
9use aws_smithy_schema::Schema;
10use aws_smithy_types::{BigDecimal, BigInteger, Blob, DateTime, Document};
11use std::fmt::Write;
12use std::ops::Deref;
13use std::sync::Arc;
14use urlencoding::encode;
15
16/// A collection path segment.
17/// - `Index(i)` — a list element: renders as `i`.
18/// - `Entry(i, name)` — a map key/value: renders as `i.name`.
19#[derive(Clone, Copy)]
20enum Segment<'a> {
21    Index(usize),
22    Entry(usize, &'a str),
23}
24
25impl std::fmt::Display for Segment<'_> {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            Segment::Index(i) => write!(f, "{i}"),
29            Segment::Entry(i, name) => write!(f, "{i}.{name}"),
30        }
31    }
32}
33
34/// A wire name held on the collection stack across serializer calls: a default, or
35/// a name shared with the [`CollectionNames`] cached on a schema.
36///
37/// Cloning never allocates.
38#[derive(Clone, Debug)]
39enum Name {
40    Static(&'static str),
41    Shared(Arc<str>),
42}
43
44impl Deref for Name {
45    type Target = str;
46
47    fn deref(&self) -> &str {
48        match self {
49            Name::Static(name) => name,
50            Name::Shared(name) => name,
51        }
52    }
53}
54
55/// The name of a collection's element (a list member or map value) or map key.
56#[derive(Debug)]
57enum ElementName {
58    /// The collection has no element schema, as for the member-less placeholder
59    /// codegen passes for a nested aggregate. Use the name inherited from the
60    /// enclosing collection.
61    Inherit,
62    /// The element schema has neither `@xmlName` nor a member name.
63    Default,
64    Named(Name),
65}
66
67impl ElementName {
68    fn of(element: Option<&Schema<'_>>) -> Self {
69        match element {
70            None => ElementName::Inherit,
71            Some(element) => match wire_name(element) {
72                Some(name) => ElementName::Named(name),
73                None => ElementName::Default,
74            },
75        }
76    }
77
78    fn resolve(&self, inherited: Option<Name>, default: &'static str) -> Name {
79        match self {
80            ElementName::Named(name) => name.clone(),
81            ElementName::Default => Name::Static(default),
82            ElementName::Inherit => inherited.unwrap_or(Name::Static(default)),
83        }
84    }
85}
86
87/// `@xmlName`, then the member name, of an element schema.
88fn wire_name(schema: &Schema<'_>) -> Option<Name> {
89    schema
90        .xml_name()
91        .map(|name| name.value())
92        .or(schema.member_name())
93        .map(|name| Name::Shared(name.into()))
94}
95
96/// The wire names a list or map schema contributes to its elements' parameter
97/// names, cached on the schema so that serializing a collection copies no names.
98#[derive(Debug)]
99struct CollectionNames {
100    /// The list member or map value name.
101    element: ElementName,
102    /// The map key name.
103    key: ElementName,
104    /// Names for a nested aggregate element that arrives with a member-less
105    /// placeholder schema (codegen passes `prelude::DOCUMENT` for nested
106    /// aggregates): its own element (or value) and key names. They are recovered
107    /// from this collection's element schema, mirroring the one-level recovery the
108    /// XML serializer performs.
109    nested_element: Option<Name>,
110    nested_key: Option<Name>,
111}
112
113static COLLECTION_NAMES: SchemaExtensionKey<CollectionNames> = SchemaExtensionKey::new(|schema| {
114    let element = schema.member();
115    CollectionNames {
116        element: ElementName::of(element),
117        key: ElementName::of(schema.key()),
118        nested_element: element.and_then(|e| e.member()).and_then(wire_name),
119        nested_key: element.and_then(|e| e.key()).and_then(wire_name),
120    }
121});
122
123enum CollectionContext {
124    List {
125        index: usize,
126        /// See [`CollectionNames::nested_element`].
127        inherited_member_name: Option<Name>,
128        /// See [`CollectionNames::nested_key`].
129        inherited_key_name: Option<Name>,
130    },
131    Map {
132        index: usize,
133        expecting_key: bool,
134        key_name: Name,
135        value_name: Name,
136        /// See [`CollectionNames::nested_element`].
137        inherited_member_name: Option<Name>,
138        /// See [`CollectionNames::nested_key`].
139        inherited_key_name: Option<Name>,
140    },
141}
142
143/// Returns the path segment for the current collection element and advances the
144/// cursor: a 1-based index for lists, `<index>.<key|value_name>` for maps. Returns
145/// `None` when not inside a collection.
146///
147/// Takes the stack rather than the serializer so the segment, which borrows its
148/// name from the stack, can be written into the serializer's other fields.
149fn next_collection_segment(context_stack: &mut [CollectionContext]) -> Option<Segment<'_>> {
150    let ctx = context_stack.last_mut()?;
151    Some(match ctx {
152        CollectionContext::List { index, .. } => {
153            let seg = Segment::Index(*index);
154            *index += 1;
155            seg
156        }
157        CollectionContext::Map {
158            index,
159            expecting_key,
160            key_name,
161            value_name,
162            ..
163        } => {
164            if *expecting_key {
165                *expecting_key = false;
166                Segment::Entry(*index, key_name)
167            } else {
168                let seg = Segment::Entry(*index, value_name);
169                *expecting_key = true;
170                *index += 1;
171                seg
172            }
173        }
174    })
175}
176
177/// Formats a [`Segment`] onto `prefix`, recording the previous length so it can be
178/// popped, without an intermediate `String`.
179fn push_prefix_segment(prefix: &mut String, prefix_lengths: &mut Vec<usize>, segment: Segment<'_>) {
180    let prev_len = prefix.len();
181    if !prefix.is_empty() {
182        prefix.push('.');
183    }
184    // Writing to a String is infallible.
185    let _ = write!(prefix, "{segment}");
186    prefix_lengths.push(prev_len);
187}
188
189/// Serializes a request shape to the awsQuery `application/x-www-form-urlencoded` body.
190///
191/// Recurses through nested structs/lists/maps without a depth bound; this is safe
192/// because the input is the client's own request shape, whose depth is fixed by the
193/// model at codegen time (not attacker-controlled).
194pub struct QueryShapeSerializer {
195    output: String,
196    prefix: String,
197    prefix_lengths: Vec<usize>,
198    context_stack: Vec<CollectionContext>,
199}
200
201impl QueryShapeSerializer {
202    pub fn new(action: &str, version: &str) -> Self {
203        let mut output = String::with_capacity(256);
204        // Writing to a String is infallible.
205        let _ = write!(
206            output,
207            "Action={}&Version={}",
208            encode(action),
209            encode(version)
210        );
211        Self {
212            output,
213            prefix: String::with_capacity(64),
214            prefix_lengths: Vec::with_capacity(8),
215            context_stack: Vec::with_capacity(4),
216        }
217    }
218
219    fn wire_name<'a>(&self, schema: &'a Schema<'_>) -> &'a str {
220        schema
221            .xml_name()
222            .map(|t| t.value())
223            .or(schema.member_name())
224            .unwrap_or("")
225    }
226
227    /// Names to hand a nested aggregate element/value that arrives with a
228    /// member-less placeholder schema (codegen passes `prelude::DOCUMENT` for
229    /// nested aggregates). The immediate parent collection resolves these from
230    /// its element/value schema while it still holds the schema borrow and
231    /// stashes them owned, so a nested `write_list`/`write_map` can recover its
232    /// `@xmlName`/member names one level down. This mirrors the one-level
233    /// recovery the XML serializer performs via its owned `list_item_name`.
234    ///
235    /// Returns `(inherited_member_name, inherited_key_name)` from the current
236    /// top-of-stack collection, but only at a map's value position
237    /// (`expecting_key == false`); map keys are always scalars.
238    fn inherited_child_names(&self) -> (Option<Name>, Option<Name>) {
239        match self.context_stack.last() {
240            Some(CollectionContext::List {
241                inherited_member_name,
242                inherited_key_name,
243                ..
244            })
245            | Some(CollectionContext::Map {
246                expecting_key: false,
247                inherited_member_name,
248                inherited_key_name,
249                ..
250            }) => (inherited_member_name.clone(), inherited_key_name.clone()),
251            _ => (None, None),
252        }
253    }
254
255    fn push_prefix(&mut self, segment: &str) {
256        let prev_len = self.prefix.len();
257        if !self.prefix.is_empty() {
258            self.prefix.push('.');
259        }
260        self.prefix.push_str(segment);
261        self.prefix_lengths.push(prev_len);
262    }
263
264    fn pop_prefix(&mut self) {
265        let prev_len = self.prefix_lengths.pop().expect("prefix stack underflow");
266        self.prefix.truncate(prev_len);
267    }
268
269    /// Appends `&<param>=<value>` to the output, where `<param>` is `prefix` joined
270    /// with either the collection segment (for an anonymous element) or the scalar's
271    /// own wire name.
272    fn write_scalar(&mut self, schema: &Schema<'_>, value: &str) -> Result<(), SerdeError> {
273        let segment = if schema.member_name().is_none() {
274            next_collection_segment(&mut self.context_stack)
275        } else {
276            None
277        };
278        self.output.push('&');
279        // Writing to a String is infallible.
280        match segment {
281            Some(seg) => {
282                let _ = write!(self.output, "{}.{}", self.prefix, seg);
283            }
284            None => {
285                let name = self.wire_name(schema);
286                // A named scalar written outside a collection must resolve to a
287                // non-empty parameter name; an empty name would emit `&=value`
288                // (or `&prefix.=value`), a malformed query param. In practice
289                // codegen always supplies a member/xmlName here — this guards
290                // against a future caller passing a member-less schema.
291                debug_assert!(
292                    !name.is_empty(),
293                    "awsQuery scalar has no wire name (would emit an empty param name)"
294                );
295                if self.prefix.is_empty() {
296                    self.output.push_str(name);
297                } else {
298                    let _ = write!(self.output, "{}.{}", self.prefix, name);
299                }
300            }
301        }
302        self.output.push('=');
303        self.output.push_str(&encode(value));
304        Ok(())
305    }
306}
307
308impl FinishSerializer for QueryShapeSerializer {
309    fn finish(self) -> Vec<u8> {
310        self.output.into_bytes()
311    }
312}
313
314impl ShapeSerializer for QueryShapeSerializer {
315    fn write_struct(
316        &mut self,
317        schema: &Schema<'_>,
318        value: &dyn SerializableStruct,
319    ) -> Result<(), SerdeError> {
320        let is_member = schema.member_name().is_some();
321        let pushed_index = if is_member {
322            self.push_prefix(self.wire_name(schema));
323            false
324        } else if let Some(seg) = next_collection_segment(&mut self.context_stack) {
325            push_prefix_segment(&mut self.prefix, &mut self.prefix_lengths, seg);
326            true
327        } else {
328            false
329        };
330        value.serialize_members(self)?;
331        if is_member || pushed_index {
332            self.pop_prefix();
333        }
334        Ok(())
335    }
336
337    fn write_list(
338        &mut self,
339        schema: &Schema<'_>,
340        write_elements: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
341    ) -> Result<(), SerdeError> {
342        // Read any name inherited from the enclosing collection BEFORE
343        // `next_collection_segment` mutates its cursor (a nested list arrives with
344        // a member-less `prelude::DOCUMENT`; its real `@xmlName`/member name is
345        // recovered from the parent here).
346        let (inherited_member, _inherited_key) = self.inherited_child_names();
347        let flat = schema.xml_flattened();
348        let is_member = schema.member_name().is_some();
349        let pushed_index = if is_member {
350            self.push_prefix(self.wire_name(schema));
351            false
352        } else if let Some(seg) = next_collection_segment(&mut self.context_stack) {
353            push_prefix_segment(&mut self.prefix, &mut self.prefix_lengths, seg);
354            true
355        } else {
356            false
357        };
358        let names = schema.extension(&COLLECTION_NAMES);
359        if !flat {
360            // This list's element name: its own member info, else the name
361            // inherited from the parent (member-less placeholder), else `member`.
362            let member_name = names.element.resolve(inherited_member, "member");
363            self.push_prefix(&member_name);
364        }
365        // Names to stash for a nested aggregate element (recovered one level down).
366        self.context_stack.push(CollectionContext::List {
367            index: 1,
368            inherited_member_name: names.nested_element.clone(),
369            inherited_key_name: names.nested_key.clone(),
370        });
371        let output_len_before = self.output.len();
372        write_elements(self)?;
373        let wrote_elements = self.output.len() > output_len_before;
374        self.context_stack.pop();
375        if !flat {
376            self.pop_prefix();
377        }
378        if !wrote_elements && is_member {
379            self.output.push('&');
380            self.output.push_str(&self.prefix);
381            self.output.push('=');
382        }
383        if is_member || pushed_index {
384            self.pop_prefix();
385        }
386        Ok(())
387    }
388
389    fn write_map(
390        &mut self,
391        schema: &Schema<'_>,
392        write_entries: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
393    ) -> Result<(), SerdeError> {
394        // Read inherited names BEFORE `next_collection_segment` mutates the
395        // enclosing collection's cursor (a nested map arrives member-less;
396        // recover its key/value names from the parent here).
397        let (inherited_member, inherited_key) = self.inherited_child_names();
398        let flat = schema.xml_flattened();
399        let is_member = schema.member_name().is_some();
400        let pushed_index = if is_member {
401            self.push_prefix(self.wire_name(schema));
402            false
403        } else if let Some(seg) = next_collection_segment(&mut self.context_stack) {
404            push_prefix_segment(&mut self.prefix, &mut self.prefix_lengths, seg);
405            true
406        } else {
407            false
408        };
409        if !flat {
410            self.push_prefix("entry");
411        }
412        // Key/value names: own member info, else inherited (member-less
413        // placeholder), else the `key`/`value` defaults.
414        let names = schema.extension(&COLLECTION_NAMES);
415        // Names to stash for a nested aggregate map value (recovered one level down).
416        self.context_stack.push(CollectionContext::Map {
417            index: 1,
418            expecting_key: true,
419            key_name: names.key.resolve(inherited_key, "key"),
420            value_name: names.element.resolve(inherited_member, "value"),
421            inherited_member_name: names.nested_element.clone(),
422            inherited_key_name: names.nested_key.clone(),
423        });
424        write_entries(self)?;
425        self.context_stack.pop();
426        if !flat {
427            self.pop_prefix();
428        }
429        if is_member || pushed_index {
430            self.pop_prefix();
431        }
432        Ok(())
433    }
434
435    fn write_string(&mut self, schema: &Schema<'_>, value: &str) -> Result<(), SerdeError> {
436        self.write_scalar(schema, value)
437    }
438
439    fn write_boolean(&mut self, schema: &Schema<'_>, value: bool) -> Result<(), SerdeError> {
440        self.write_scalar(schema, if value { "true" } else { "false" })
441    }
442
443    fn write_byte(&mut self, schema: &Schema<'_>, value: i8) -> Result<(), SerdeError> {
444        self.write_scalar(schema, &value.to_string())
445    }
446
447    fn write_short(&mut self, schema: &Schema<'_>, value: i16) -> Result<(), SerdeError> {
448        self.write_scalar(schema, &value.to_string())
449    }
450
451    fn write_integer(&mut self, schema: &Schema<'_>, value: i32) -> Result<(), SerdeError> {
452        self.write_scalar(schema, &value.to_string())
453    }
454
455    fn write_long(&mut self, schema: &Schema<'_>, value: i64) -> Result<(), SerdeError> {
456        self.write_scalar(schema, &value.to_string())
457    }
458
459    fn write_float(&mut self, schema: &Schema<'_>, value: f32) -> Result<(), SerdeError> {
460        let s = if value.is_nan() {
461            "NaN".to_string()
462        } else if value == f32::INFINITY {
463            "Infinity".to_string()
464        } else if value == f32::NEG_INFINITY {
465            "-Infinity".to_string()
466        } else {
467            aws_smithy_types::primitive::Encoder::from(value)
468                .encode()
469                .to_owned()
470        };
471        self.write_scalar(schema, &s)
472    }
473
474    fn write_double(&mut self, schema: &Schema<'_>, value: f64) -> Result<(), SerdeError> {
475        let s = if value.is_nan() {
476            "NaN".to_string()
477        } else if value == f64::INFINITY {
478            "Infinity".to_string()
479        } else if value == f64::NEG_INFINITY {
480            "-Infinity".to_string()
481        } else {
482            aws_smithy_types::primitive::Encoder::from(value)
483                .encode()
484                .to_owned()
485        };
486        self.write_scalar(schema, &s)
487    }
488
489    fn write_big_integer(
490        &mut self,
491        schema: &Schema<'_>,
492        value: &BigInteger,
493    ) -> Result<(), SerdeError> {
494        self.write_scalar(schema, value.as_ref())
495    }
496
497    fn write_big_decimal(
498        &mut self,
499        schema: &Schema<'_>,
500        value: &BigDecimal,
501    ) -> Result<(), SerdeError> {
502        self.write_scalar(schema, value.as_ref())
503    }
504
505    fn write_blob(&mut self, schema: &Schema<'_>, value: Blob) -> Result<(), SerdeError> {
506        self.write_scalar(schema, &aws_smithy_types::base64::encode(value.as_ref()))
507    }
508
509    fn write_timestamp(&mut self, schema: &Schema<'_>, value: &DateTime) -> Result<(), SerdeError> {
510        let format = if let Some(ts_trait) = schema.timestamp_format() {
511            match ts_trait.format() {
512                aws_smithy_schema::traits::TimestampFormat::EpochSeconds => {
513                    aws_smithy_types::date_time::Format::EpochSeconds
514                }
515                aws_smithy_schema::traits::TimestampFormat::HttpDate => {
516                    aws_smithy_types::date_time::Format::HttpDate
517                }
518                aws_smithy_schema::traits::TimestampFormat::DateTime => {
519                    aws_smithy_types::date_time::Format::DateTime
520                }
521                other => {
522                    return Err(SerdeError::unsupported(format!(
523                        "unsupported timestamp format {other:?}"
524                    )))
525                }
526            }
527        } else {
528            aws_smithy_types::date_time::Format::DateTime
529        };
530        let formatted = value
531            .fmt(format)
532            .map_err(|e| SerdeError::custom(format!("timestamp format error: {e}")))?;
533        self.write_scalar(schema, &formatted)
534    }
535
536    fn write_document(&mut self, _: &Schema<'_>, _: &Document) -> Result<(), SerdeError> {
537        Err(SerdeError::unsupported(
538            "documents not supported in awsQuery",
539        ))
540    }
541
542    fn write_null(&mut self, _: &Schema<'_>) -> Result<(), SerdeError> {
543        // awsQuery has no null representation: nulls are omitted. The collection
544        // index is not advanced here (only emitted elements consume an index), so a
545        // `@sparse` null is dropped rather than reserving a positional slot.
546        Ok(())
547    }
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553    use aws_smithy_schema::{shape_id, ShapeType};
554
555    static NAME_MEMBER: Schema<'static> =
556        Schema::new_member(shape_id!("test", "Input"), ShapeType::String, "Name", 0);
557    static AGE_MEMBER: Schema<'static> =
558        Schema::new_member(shape_id!("test", "Input"), ShapeType::Integer, "Age", 1);
559    static INPUT_SCHEMA: Schema<'static> = Schema::new_struct(
560        shape_id!("test", "Input"),
561        ShapeType::Structure,
562        &[&NAME_MEMBER, &AGE_MEMBER],
563    );
564
565    struct SimpleInput;
566    impl SerializableStruct for SimpleInput {
567        fn schema(&self) -> &Schema<'_> {
568            &INPUT_SCHEMA
569        }
570
571        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
572            s.write_string(&NAME_MEMBER, "Alice")?;
573            s.write_integer(&AGE_MEMBER, 30)
574        }
575    }
576
577    #[test]
578    fn simple_struct() {
579        let mut ser = QueryShapeSerializer::new("GetUser", "2012-11-05");
580        ser.write_struct(&INPUT_SCHEMA, &SimpleInput).unwrap();
581        let output = String::from_utf8(ser.finish()).unwrap();
582        assert_eq!(
583            output,
584            "Action=GetUser&Version=2012-11-05&Name=Alice&Age=30"
585        );
586    }
587
588    #[test]
589    fn boolean_values() {
590        static M: Schema<'static> =
591            Schema::new_member(shape_id!("test", "I"), ShapeType::Boolean, "Verbose", 0);
592        let mut ser = QueryShapeSerializer::new("Op", "1.0");
593        ser.write_boolean(&M, true).unwrap();
594        assert_eq!(
595            String::from_utf8(ser.finish()).unwrap(),
596            "Action=Op&Version=1.0&Verbose=true"
597        );
598    }
599
600    #[test]
601    fn string_encoding() {
602        static M: Schema<'static> =
603            Schema::new_member(shape_id!("test", "I"), ShapeType::String, "Message", 0);
604        let mut ser = QueryShapeSerializer::new("Op", "1.0");
605        ser.write_string(&M, "hello world&foo=bar").unwrap();
606        assert_eq!(
607            String::from_utf8(ser.finish()).unwrap(),
608            "Action=Op&Version=1.0&Message=hello%20world%26foo%3Dbar"
609        );
610    }
611
612    #[test]
613    fn nested_struct() {
614        static INNER_FIELD: Schema<'static> =
615            Schema::new_member(shape_id!("test", "Inner"), ShapeType::String, "Value", 0);
616        static OUTER_MEMBER: Schema<'static> = Schema::new_member(
617            shape_id!("test", "Outer"),
618            ShapeType::Structure,
619            "Config",
620            0,
621        );
622
623        struct Inner;
624        impl SerializableStruct for Inner {
625            fn schema(&self) -> &Schema<'_> {
626                static SCHEMA: Schema<'static> = Schema::new_struct(
627                    aws_smithy_schema::shape_id!("test", "Inner"),
628                    aws_smithy_schema::ShapeType::Structure,
629                    &[&INNER_FIELD],
630                );
631                &SCHEMA
632            }
633
634            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
635                s.write_string(&INNER_FIELD, "hello")
636            }
637        }
638
639        let mut ser = QueryShapeSerializer::new("Op", "1.0");
640        ser.write_struct(&OUTER_MEMBER, &Inner).unwrap();
641        assert_eq!(
642            String::from_utf8(ser.finish()).unwrap(),
643            "Action=Op&Version=1.0&Config.Value=hello"
644        );
645    }
646
647    #[test]
648    fn list_non_flat() {
649        static M: Schema<'static> =
650            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "Items", 0);
651        let mut ser = QueryShapeSerializer::new("Op", "1.0");
652        ser.write_list(&M, &|s| {
653            s.write_string(&aws_smithy_schema::prelude::STRING, "foo")?;
654            s.write_string(&aws_smithy_schema::prelude::STRING, "bar")
655        })
656        .unwrap();
657        assert_eq!(
658            String::from_utf8(ser.finish()).unwrap(),
659            "Action=Op&Version=1.0&Items.member.1=foo&Items.member.2=bar"
660        );
661    }
662
663    #[test]
664    fn list_flat() {
665        static M: Schema<'static> =
666            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "Items", 0)
667                .with_xml_flattened();
668        let mut ser = QueryShapeSerializer::new("Op", "1.0");
669        ser.write_list(&M, &|s| {
670            s.write_string(&aws_smithy_schema::prelude::STRING, "A")?;
671            s.write_string(&aws_smithy_schema::prelude::STRING, "B")
672        })
673        .unwrap();
674        assert_eq!(
675            String::from_utf8(ser.finish()).unwrap(),
676            "Action=Op&Version=1.0&Items.1=A&Items.2=B"
677        );
678    }
679
680    #[test]
681    fn list_of_integers() {
682        static M: Schema<'static> =
683            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "Ids", 0);
684        let mut ser = QueryShapeSerializer::new("Op", "1.0");
685        ser.write_list(&M, &|s| {
686            s.write_integer(&aws_smithy_schema::prelude::INTEGER, 10)?;
687            s.write_integer(&aws_smithy_schema::prelude::INTEGER, 20)?;
688            s.write_integer(&aws_smithy_schema::prelude::INTEGER, 30)
689        })
690        .unwrap();
691        assert_eq!(
692            String::from_utf8(ser.finish()).unwrap(),
693            "Action=Op&Version=1.0&Ids.member.1=10&Ids.member.2=20&Ids.member.3=30"
694        );
695    }
696
697    #[test]
698    fn map_non_flat() {
699        static M: Schema<'static> =
700            Schema::new_member(shape_id!("test", "I"), ShapeType::Map, "Tags", 0);
701        let mut ser = QueryShapeSerializer::new("Op", "1.0");
702        ser.write_map(&M, &|s| {
703            s.write_string(&aws_smithy_schema::prelude::STRING, "color")?;
704            s.write_string(&aws_smithy_schema::prelude::STRING, "red")?;
705            s.write_string(&aws_smithy_schema::prelude::STRING, "size")?;
706            s.write_string(&aws_smithy_schema::prelude::STRING, "large")
707        })
708        .unwrap();
709        assert_eq!(
710            String::from_utf8(ser.finish()).unwrap(),
711            "Action=Op&Version=1.0\
712             &Tags.entry.1.key=color&Tags.entry.1.value=red\
713             &Tags.entry.2.key=size&Tags.entry.2.value=large"
714        );
715    }
716
717    #[test]
718    fn list_non_flat_uses_renamed_member_name() {
719        // @xmlName on the list's inner member schema (attached via
720        // with_list_member, exactly as codegen's emitAggregateMemberChain
721        // does) drives the repeated element name — no protocol-specific
722        // schema field required. Mirrors the AWS REST XML serializer.
723        static ITEM: Schema<'static> = Schema::new_member(
724            shape_id!("test", "L$member"),
725            ShapeType::String,
726            "member",
727            0,
728        )
729        .with_xml_name("Item");
730        static M: Schema<'static> =
731            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "Items", 0)
732                .with_list_member(&ITEM);
733        let mut ser = QueryShapeSerializer::new("Op", "1.0");
734        ser.write_list(&M, &|s| {
735            s.write_string(&aws_smithy_schema::prelude::STRING, "foo")?;
736            s.write_string(&aws_smithy_schema::prelude::STRING, "bar")
737        })
738        .unwrap();
739        assert_eq!(
740            String::from_utf8(ser.finish()).unwrap(),
741            "Action=Op&Version=1.0&Items.Item.1=foo&Items.Item.2=bar"
742        );
743    }
744
745    #[test]
746    fn map_non_flat_uses_renamed_key_value_names() {
747        // @xmlName on the map's key/value member schemas (attached via
748        // with_map_members) drives the entry key/value element names.
749        static KEY: Schema<'static> =
750            Schema::new_member(shape_id!("test", "M$key"), ShapeType::String, "key", 0)
751                .with_xml_name("Attribute");
752        static VALUE: Schema<'static> =
753            Schema::new_member(shape_id!("test", "M$value"), ShapeType::String, "value", 1)
754                .with_xml_name("Setting");
755        static M: Schema<'static> =
756            Schema::new_member(shape_id!("test", "I"), ShapeType::Map, "Tags", 0)
757                .with_map_members(&KEY, &VALUE);
758        let mut ser = QueryShapeSerializer::new("Op", "1.0");
759        ser.write_map(&M, &|s| {
760            s.write_string(&aws_smithy_schema::prelude::STRING, "color")?;
761            s.write_string(&aws_smithy_schema::prelude::STRING, "red")
762        })
763        .unwrap();
764        assert_eq!(
765            String::from_utf8(ser.finish()).unwrap(),
766            "Action=Op&Version=1.0&Tags.entry.1.Attribute=color&Tags.entry.1.Setting=red"
767        );
768    }
769
770    #[test]
771    fn map_flat() {
772        static M: Schema<'static> =
773            Schema::new_member(shape_id!("test", "I"), ShapeType::Map, "Tags", 0)
774                .with_xml_flattened();
775        let mut ser = QueryShapeSerializer::new("Op", "1.0");
776        ser.write_map(&M, &|s| {
777            s.write_string(&aws_smithy_schema::prelude::STRING, "k1")?;
778            s.write_string(&aws_smithy_schema::prelude::STRING, "v1")
779        })
780        .unwrap();
781        assert_eq!(
782            String::from_utf8(ser.finish()).unwrap(),
783            "Action=Op&Version=1.0&Tags.1.key=k1&Tags.1.value=v1"
784        );
785    }
786}
787
788#[cfg(test)]
789mod cross_validation {
790    use super::*;
791    use crate::QueryWriter;
792    use aws_smithy_schema::{shape_id, ShapeType};
793
794    #[test]
795    fn matches_query_writer_simple_params() {
796        let mut expected = String::new();
797        let mut writer = QueryWriter::new(&mut expected, "SomeAction", "1.0");
798        writer.prefix("Name").string("Alice");
799        writer
800            .prefix("Age")
801            .number(aws_smithy_types::Number::PosInt(30));
802        writer.finish();
803
804        static NAME: Schema<'static> =
805            Schema::new_member(shape_id!("test", "I"), ShapeType::String, "Name", 0);
806        static AGE: Schema<'static> =
807            Schema::new_member(shape_id!("test", "I"), ShapeType::Integer, "Age", 1);
808        static SCHEMA: Schema<'static> =
809            Schema::new_struct(shape_id!("test", "I"), ShapeType::Structure, &[&NAME, &AGE]);
810        struct Input;
811        impl SerializableStruct for Input {
812            fn schema(&self) -> &Schema<'_> {
813                &SCHEMA
814            }
815
816            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
817                s.write_string(&NAME, "Alice")?;
818                s.write_integer(&AGE, 30)
819            }
820        }
821
822        let mut ser = QueryShapeSerializer::new("SomeAction", "1.0");
823        ser.write_struct(&SCHEMA, &Input).unwrap();
824        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
825    }
826
827    #[test]
828    fn matches_query_writer_list() {
829        let mut expected = String::new();
830        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
831        let mut list = writer.prefix("ListArg").start_list(false, None);
832        list.entry().string("foo");
833        list.entry().string("bar");
834        list.entry().string("baz");
835        list.finish();
836        writer.finish();
837
838        static M: Schema<'static> =
839            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "ListArg", 0);
840        let mut ser = QueryShapeSerializer::new("Op", "1.0");
841        ser.write_list(&M, &|s| {
842            s.write_string(&aws_smithy_schema::prelude::STRING, "foo")?;
843            s.write_string(&aws_smithy_schema::prelude::STRING, "bar")?;
844            s.write_string(&aws_smithy_schema::prelude::STRING, "baz")
845        })
846        .unwrap();
847        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
848    }
849
850    #[test]
851    fn matches_query_writer_flat_list() {
852        let mut expected = String::new();
853        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
854        let mut list = writer.prefix("FlatList").start_list(true, None);
855        list.entry().string("A");
856        list.entry().string("B");
857        list.finish();
858        writer.finish();
859
860        static M: Schema<'static> =
861            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "FlatList", 0)
862                .with_xml_flattened();
863        let mut ser = QueryShapeSerializer::new("Op", "1.0");
864        ser.write_list(&M, &|s| {
865            s.write_string(&aws_smithy_schema::prelude::STRING, "A")?;
866            s.write_string(&aws_smithy_schema::prelude::STRING, "B")
867        })
868        .unwrap();
869        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
870    }
871
872    #[test]
873    fn matches_query_writer_map() {
874        let mut expected = String::new();
875        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
876        let mut map = writer.prefix("MapArg").start_map(false, "key", "value");
877        map.entry("bar").string("Bar");
878        map.entry("foo").string("Foo");
879        map.finish();
880        writer.finish();
881
882        static M: Schema<'static> =
883            Schema::new_member(shape_id!("test", "I"), ShapeType::Map, "MapArg", 0);
884        let mut ser = QueryShapeSerializer::new("Op", "1.0");
885        ser.write_map(&M, &|s| {
886            s.write_string(&aws_smithy_schema::prelude::STRING, "bar")?;
887            s.write_string(&aws_smithy_schema::prelude::STRING, "Bar")?;
888            s.write_string(&aws_smithy_schema::prelude::STRING, "foo")?;
889            s.write_string(&aws_smithy_schema::prelude::STRING, "Foo")
890        })
891        .unwrap();
892        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
893    }
894
895    #[test]
896    fn matches_query_writer_nested_prefix() {
897        let mut expected = String::new();
898        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
899        writer.prefix("first").prefix("second").string("val");
900        writer.finish();
901
902        static SECOND: Schema<'static> =
903            Schema::new_member(shape_id!("test", "Inner"), ShapeType::String, "second", 0);
904        static FIRST: Schema<'static> =
905            Schema::new_member(shape_id!("test", "Outer"), ShapeType::Structure, "first", 0);
906
907        struct Inner;
908        impl SerializableStruct for Inner {
909            fn schema(&self) -> &Schema<'_> {
910                static SCHEMA: Schema<'static> = Schema::new_struct(
911                    aws_smithy_schema::shape_id!("test", "Inner"),
912                    aws_smithy_schema::ShapeType::Structure,
913                    &[&SECOND],
914                );
915                &SCHEMA
916            }
917
918            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
919                s.write_string(&SECOND, "val")
920            }
921        }
922
923        let mut ser = QueryShapeSerializer::new("Op", "1.0");
924        ser.write_struct(&FIRST, &Inner).unwrap();
925        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
926    }
927
928    #[test]
929    fn nested_renamed_inner_list_member_matches_legacy() {
930        // Regression: a `map<string, list<string>>` whose inner list member has
931        // `@xmlName("item")`. Codegen invokes the inner `write_list` with a
932        // member-less `prelude::DOCUMENT`, so the rename must be recovered from
933        // the outer map's value-member schema (stashed as an owned inherited
934        // name). Previously this fell back to "member"; now it must honor "item"
935        // and match the legacy `QueryWriter`.
936        static INNER_MEMBER: Schema<'static> = Schema::new_member(
937            shape_id!("test", "L$member"),
938            ShapeType::String,
939            "member",
940            0,
941        )
942        .with_xml_name("item");
943        // The map's value member is a list carrying the renamed inner member.
944        static VALUE_LIST: Schema<'static> =
945            Schema::new_member(shape_id!("test", "M$value"), ShapeType::List, "value", 1)
946                .with_list_member(&INNER_MEMBER);
947        static KEY: Schema<'static> =
948            Schema::new_member(shape_id!("test", "M$key"), ShapeType::String, "key", 0);
949        static OUTER_MAP: Schema<'static> =
950            Schema::new_member(shape_id!("test", "I"), ShapeType::Map, "MapOfLists", 0)
951                .with_map_members(&KEY, &VALUE_LIST);
952
953        let mut ser = QueryShapeSerializer::new("Op", "1.0");
954        ser.write_map(&OUTER_MAP, &|s| {
955            s.write_string(&aws_smithy_schema::prelude::STRING, "bar")?;
956            // codegen emits `write_list(&prelude::DOCUMENT, ..)` for the inner list
957            s.write_list(&aws_smithy_schema::prelude::DOCUMENT, &|inner| {
958                inner.write_string(&aws_smithy_schema::prelude::STRING, "C")?;
959                inner.write_string(&aws_smithy_schema::prelude::STRING, "D")
960            })
961        })
962        .unwrap();
963        let schema_out = String::from_utf8(ser.finish()).unwrap();
964
965        let mut expected = String::new();
966        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
967        let mut map = writer.prefix("MapOfLists").start_map(false, "key", "value");
968        {
969            let mut list = map.entry("bar").start_list(false, Some("item"));
970            list.entry().string("C");
971            list.entry().string("D");
972            list.finish();
973        }
974        map.finish();
975        writer.finish();
976
977        assert_eq!(schema_out, expected);
978        assert_eq!(
979            schema_out,
980            "Action=Op&Version=1.0&MapOfLists.entry.1.key=bar\
981             &MapOfLists.entry.1.value.item.1=C\
982             &MapOfLists.entry.1.value.item.2=D"
983        );
984    }
985
986    #[test]
987    fn nested_renamed_inner_map_members_matches_legacy() {
988        // Same fix for a `map<string, map<string, string>>` whose INNER map has
989        // `@xmlName` on its key/value members. The inner `write_map` gets
990        // `prelude::DOCUMENT`; the key/value renames come from the outer map's
991        // value-member schema.
992        static INNER_KEY: Schema<'static> =
993            Schema::new_member(shape_id!("test", "IM$key"), ShapeType::String, "key", 0)
994                .with_xml_name("K");
995        static INNER_VALUE: Schema<'static> =
996            Schema::new_member(shape_id!("test", "IM$value"), ShapeType::String, "value", 1)
997                .with_xml_name("V");
998        // Outer map value member is itself a map with renamed key/value.
999        static VALUE_MAP: Schema<'static> =
1000            Schema::new_member(shape_id!("test", "M$value"), ShapeType::Map, "value", 1)
1001                .with_map_members(&INNER_KEY, &INNER_VALUE);
1002        static OUTER_KEY: Schema<'static> =
1003            Schema::new_member(shape_id!("test", "M$key"), ShapeType::String, "key", 0);
1004        static OUTER_MAP: Schema<'static> =
1005            Schema::new_member(shape_id!("test", "I"), ShapeType::Map, "MapOfMaps", 0)
1006                .with_map_members(&OUTER_KEY, &VALUE_MAP);
1007
1008        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1009        ser.write_map(&OUTER_MAP, &|s| {
1010            s.write_string(&aws_smithy_schema::prelude::STRING, "outer")?;
1011            s.write_map(&aws_smithy_schema::prelude::DOCUMENT, &|inner| {
1012                inner.write_string(&aws_smithy_schema::prelude::STRING, "ik")?;
1013                inner.write_string(&aws_smithy_schema::prelude::STRING, "iv")
1014            })
1015        })
1016        .unwrap();
1017        let schema_out = String::from_utf8(ser.finish()).unwrap();
1018
1019        assert_eq!(
1020            schema_out,
1021            "Action=Op&Version=1.0&MapOfMaps.entry.1.key=outer\
1022             &MapOfMaps.entry.1.value.entry.1.K=ik\
1023             &MapOfMaps.entry.1.value.entry.1.V=iv"
1024        );
1025    }
1026}
1027
1028#[cfg(test)]
1029mod edge_cases {
1030    use super::*;
1031    use crate::QueryWriter;
1032    use aws_smithy_schema::{shape_id, ShapeType};
1033
1034    #[test]
1035    fn empty_string_value() {
1036        let mut expected = String::new();
1037        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
1038        writer.prefix("Empty").string("");
1039        writer.finish();
1040
1041        static M: Schema<'static> =
1042            Schema::new_member(shape_id!("test", "I"), ShapeType::String, "Empty", 0);
1043        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1044        ser.write_string(&M, "").unwrap();
1045        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
1046    }
1047
1048    #[test]
1049    fn empty_non_flat_list_emits_bare_param() {
1050        // An empty list serializes to just `<prefix>=`; cross-checked against the legacy writer.
1051        let mut expected = String::new();
1052        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
1053        writer.prefix("myList").start_list(false, None).finish();
1054        writer.finish();
1055
1056        static M: Schema<'static> =
1057            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "myList", 0);
1058        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1059        ser.write_list(&M, &|_| Ok(())).unwrap();
1060        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
1061        assert_eq!(expected, "Action=Op&Version=1.0&myList=");
1062    }
1063
1064    #[test]
1065    fn empty_flat_list_emits_bare_param() {
1066        let mut expected = String::new();
1067        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
1068        writer.prefix("myList").start_list(true, None).finish();
1069        writer.finish();
1070
1071        static M: Schema<'static> =
1072            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "myList", 0)
1073                .with_xml_flattened();
1074        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1075        ser.write_list(&M, &|_| Ok(())).unwrap();
1076        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
1077        assert_eq!(expected, "Action=Op&Version=1.0&myList=");
1078    }
1079
1080    #[test]
1081    fn empty_list_does_not_alias_single_empty_string_element() {
1082        // Guards the `wrote_elements` length-delta heuristic in `write_list`: an empty
1083        // list (`myList=`) must not produce the same output as a one-empty-string-element
1084        // list (`myList.member.1=`).
1085        static EMPTY: Schema<'static> =
1086            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "myList", 0);
1087        let mut empty_ser = QueryShapeSerializer::new("Op", "1.0");
1088        empty_ser.write_list(&EMPTY, &|_| Ok(())).unwrap();
1089        let empty_out = String::from_utf8(empty_ser.finish()).unwrap();
1090
1091        static ONE: Schema<'static> =
1092            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "myList", 0);
1093        let mut one_ser = QueryShapeSerializer::new("Op", "1.0");
1094        one_ser
1095            .write_list(&ONE, &|s| {
1096                s.write_string(&aws_smithy_schema::prelude::STRING, "")
1097            })
1098            .unwrap();
1099        let one_out = String::from_utf8(one_ser.finish()).unwrap();
1100
1101        assert_eq!(empty_out, "Action=Op&Version=1.0&myList=");
1102        assert_eq!(one_out, "Action=Op&Version=1.0&myList.member.1=");
1103        assert_ne!(empty_out, one_out);
1104    }
1105
1106    #[test]
1107    fn sparse_null_list_element_is_dropped_and_index_reflects_emitted_only() {
1108        // A null element is dropped without consuming an index, so the next real
1109        // element takes `.2` rather than `.3`.
1110        static M: Schema<'static> =
1111            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "Items", 0);
1112        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1113        ser.write_list(&M, &|s| {
1114            s.write_string(&aws_smithy_schema::prelude::STRING, "a")?;
1115            s.write_null(&aws_smithy_schema::prelude::STRING)?;
1116            s.write_string(&aws_smithy_schema::prelude::STRING, "b")
1117        })
1118        .unwrap();
1119        assert_eq!(
1120            String::from_utf8(ser.finish()).unwrap(),
1121            "Action=Op&Version=1.0&Items.member.1=a&Items.member.2=b"
1122        );
1123    }
1124
1125    #[test]
1126    fn special_characters() {
1127        let mut expected = String::new();
1128        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
1129        writer.prefix("Val").string("a=b&c<d>e\"f");
1130        writer.finish();
1131
1132        static M: Schema<'static> =
1133            Schema::new_member(shape_id!("test", "I"), ShapeType::String, "Val", 0);
1134        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1135        ser.write_string(&M, "a=b&c<d>e\"f").unwrap();
1136        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
1137    }
1138
1139    #[test]
1140    fn unicode_value() {
1141        let mut expected = String::new();
1142        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
1143        writer.prefix("Name").string("日本語");
1144        writer.finish();
1145
1146        static M: Schema<'static> =
1147            Schema::new_member(shape_id!("test", "I"), ShapeType::String, "Name", 0);
1148        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1149        ser.write_string(&M, "日本語").unwrap();
1150        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
1151    }
1152
1153    #[test]
1154    fn timestamp_epoch_seconds() {
1155        let mut expected = String::new();
1156        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
1157        writer
1158            .prefix("Time")
1159            .date_time(
1160                &aws_smithy_types::DateTime::from_secs(1700000000),
1161                aws_smithy_types::date_time::Format::DateTime,
1162            )
1163            .unwrap();
1164        writer.finish();
1165
1166        static M: Schema<'static> =
1167            Schema::new_member(shape_id!("test", "I"), ShapeType::Timestamp, "Time", 0);
1168        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1169        ser.write_timestamp(&M, &aws_smithy_types::DateTime::from_secs(1700000000))
1170            .unwrap();
1171        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
1172    }
1173
1174    #[test]
1175    fn deeply_nested_struct() {
1176        let mut expected = String::new();
1177        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
1178        writer.prefix("A").prefix("B").prefix("C").string("deep");
1179        writer.finish();
1180
1181        static C_FIELD: Schema<'static> =
1182            Schema::new_member(shape_id!("t", "C"), ShapeType::String, "C", 0);
1183        static B_MEMBER: Schema<'static> =
1184            Schema::new_member(shape_id!("t", "B"), ShapeType::Structure, "B", 0);
1185        static A_MEMBER: Schema<'static> =
1186            Schema::new_member(shape_id!("t", "A"), ShapeType::Structure, "A", 0);
1187
1188        struct CStruct;
1189        impl SerializableStruct for CStruct {
1190            fn schema(&self) -> &Schema<'_> {
1191                static SCHEMA: Schema<'static> = Schema::new_struct(
1192                    aws_smithy_schema::shape_id!("t", "C"),
1193                    aws_smithy_schema::ShapeType::Structure,
1194                    &[&C_FIELD],
1195                );
1196                &SCHEMA
1197            }
1198
1199            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1200                s.write_string(&C_FIELD, "deep")
1201            }
1202        }
1203        struct BStruct;
1204        impl SerializableStruct for BStruct {
1205            fn schema(&self) -> &Schema<'_> {
1206                static SCHEMA: Schema<'static> = Schema::new_struct(
1207                    aws_smithy_schema::shape_id!("t", "B"),
1208                    aws_smithy_schema::ShapeType::Structure,
1209                    &[&B_MEMBER],
1210                );
1211                &SCHEMA
1212            }
1213
1214            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1215                s.write_struct(&B_MEMBER, &CStruct)
1216            }
1217        }
1218
1219        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1220        ser.write_struct(&A_MEMBER, &BStruct).unwrap();
1221        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
1222    }
1223
1224    #[test]
1225    fn list_inside_struct() {
1226        let mut expected = String::new();
1227        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
1228        let mut outer = writer.prefix("Outer");
1229        let mut list = outer.prefix("Items").start_list(false, None);
1230        list.entry().string("x");
1231        list.entry().string("y");
1232        list.finish();
1233        writer.finish();
1234
1235        static ITEMS: Schema<'static> =
1236            Schema::new_member(shape_id!("t", "S"), ShapeType::List, "Items", 0);
1237        static OUTER: Schema<'static> =
1238            Schema::new_member(shape_id!("t", "T"), ShapeType::Structure, "Outer", 0);
1239
1240        struct Inner;
1241        impl SerializableStruct for Inner {
1242            fn schema(&self) -> &Schema<'_> {
1243                static SCHEMA: Schema<'static> = Schema::new_struct(
1244                    aws_smithy_schema::shape_id!("t", "T"),
1245                    aws_smithy_schema::ShapeType::Structure,
1246                    &[&ITEMS],
1247                );
1248                &SCHEMA
1249            }
1250
1251            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1252                s.write_list(&ITEMS, &|s| {
1253                    s.write_string(&aws_smithy_schema::prelude::STRING, "x")?;
1254                    s.write_string(&aws_smithy_schema::prelude::STRING, "y")
1255                })
1256            }
1257        }
1258
1259        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1260        ser.write_struct(&OUTER, &Inner).unwrap();
1261        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
1262    }
1263
1264    #[test]
1265    fn multiple_fields_with_list_and_scalar() {
1266        let mut expected = String::new();
1267        let mut writer = QueryWriter::new(&mut expected, "Op", "1.0");
1268        writer.prefix("Name").string("Alice");
1269        let mut list = writer.prefix("Tags").start_list(false, None);
1270        list.entry().string("admin");
1271        list.entry().string("user");
1272        list.finish();
1273        writer
1274            .prefix("Age")
1275            .number(aws_smithy_types::Number::PosInt(30));
1276        writer.finish();
1277
1278        static NAME: Schema<'static> =
1279            Schema::new_member(shape_id!("t", "S"), ShapeType::String, "Name", 0);
1280        static TAGS: Schema<'static> =
1281            Schema::new_member(shape_id!("t", "S"), ShapeType::List, "Tags", 1);
1282        static AGE: Schema<'static> =
1283            Schema::new_member(shape_id!("t", "S"), ShapeType::Integer, "Age", 2);
1284        static SCHEMA: Schema<'static> = Schema::new_struct(
1285            shape_id!("t", "S"),
1286            ShapeType::Structure,
1287            &[&NAME, &TAGS, &AGE],
1288        );
1289
1290        struct Input;
1291        impl SerializableStruct for Input {
1292            fn schema(&self) -> &Schema<'_> {
1293                &SCHEMA
1294            }
1295
1296            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1297                s.write_string(&NAME, "Alice")?;
1298                s.write_list(&TAGS, &|s| {
1299                    s.write_string(&aws_smithy_schema::prelude::STRING, "admin")?;
1300                    s.write_string(&aws_smithy_schema::prelude::STRING, "user")
1301                })?;
1302                s.write_integer(&AGE, 30)
1303            }
1304        }
1305
1306        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1307        ser.write_struct(&SCHEMA, &Input).unwrap();
1308        assert_eq!(String::from_utf8(ser.finish()).unwrap(), expected);
1309    }
1310
1311    #[test]
1312    fn list_of_struct_emits_per_element_index() {
1313        static NAME: Schema<'static> =
1314            Schema::new_member(shape_id!("test", "Item"), ShapeType::String, "Name", 0);
1315        static ITEM_SCHEMA: Schema<'static> =
1316            Schema::new_struct(shape_id!("test", "Item"), ShapeType::Structure, &[&NAME]);
1317        static ITEMS_LIST: Schema<'static> =
1318            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "Items", 0);
1319
1320        struct Item(&'static str);
1321        impl SerializableStruct for Item {
1322            fn schema(&self) -> &Schema<'_> {
1323                &ITEM_SCHEMA
1324            }
1325
1326            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1327                s.write_string(&NAME, self.0)
1328            }
1329        }
1330
1331        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1332        ser.write_list(&ITEMS_LIST, &|s| {
1333            s.write_struct(&ITEM_SCHEMA, &Item("X1"))?;
1334            s.write_struct(&ITEM_SCHEMA, &Item("X2"))
1335        })
1336        .unwrap();
1337
1338        assert_eq!(
1339            String::from_utf8(ser.finish()).unwrap(),
1340            "Action=Op&Version=1.0&Items.member.1.Name=X1&Items.member.2.Name=X2",
1341        );
1342    }
1343
1344    #[test]
1345    fn list_of_list() {
1346        static OUTER: Schema<'static> =
1347            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "Outer", 0);
1348        static INNER_LIST: Schema<'static> = Schema::new_list(
1349            shape_id!("test", "InnerList"),
1350            &aws_smithy_schema::prelude::STRING,
1351        );
1352
1353        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1354        ser.write_list(&OUTER, &|s_outer| {
1355            s_outer.write_list(&INNER_LIST, &|s_inner| {
1356                s_inner.write_string(&aws_smithy_schema::prelude::STRING, "a")?;
1357                s_inner.write_string(&aws_smithy_schema::prelude::STRING, "b")
1358            })?;
1359            s_outer.write_list(&INNER_LIST, &|s_inner| {
1360                s_inner.write_string(&aws_smithy_schema::prelude::STRING, "c")
1361            })
1362        })
1363        .unwrap();
1364
1365        assert_eq!(
1366            String::from_utf8(ser.finish()).unwrap(),
1367            "Action=Op&Version=1.0\
1368             &Outer.member.1.member.1=a\
1369             &Outer.member.1.member.2=b\
1370             &Outer.member.2.member.1=c",
1371        );
1372    }
1373
1374    #[test]
1375    fn map_of_struct_emits_per_entry_index() {
1376        static NAME: Schema<'static> =
1377            Schema::new_member(shape_id!("test", "Val"), ShapeType::String, "Name", 0);
1378        static VAL_SCHEMA: Schema<'static> =
1379            Schema::new_struct(shape_id!("test", "Val"), ShapeType::Structure, &[&NAME]);
1380        static MAP_MEMBER: Schema<'static> =
1381            Schema::new_member(shape_id!("test", "I"), ShapeType::Map, "Things", 0);
1382
1383        struct Val(&'static str);
1384        impl SerializableStruct for Val {
1385            fn schema(&self) -> &Schema<'_> {
1386                &VAL_SCHEMA
1387            }
1388
1389            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1390                s.write_string(&NAME, self.0)
1391            }
1392        }
1393
1394        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1395        ser.write_map(&MAP_MEMBER, &|s| {
1396            s.write_string(&aws_smithy_schema::prelude::STRING, "k1")?;
1397            s.write_struct(&VAL_SCHEMA, &Val("v1"))?;
1398            s.write_string(&aws_smithy_schema::prelude::STRING, "k2")?;
1399            s.write_struct(&VAL_SCHEMA, &Val("v2"))
1400        })
1401        .unwrap();
1402
1403        assert_eq!(
1404            String::from_utf8(ser.finish()).unwrap(),
1405            "Action=Op&Version=1.0\
1406             &Things.entry.1.key=k1&Things.entry.1.value.Name=v1\
1407             &Things.entry.2.key=k2&Things.entry.2.value.Name=v2",
1408        );
1409    }
1410
1411    #[test]
1412    fn map_nested_in_list() {
1413        static MAP_SCHEMA: Schema<'static> = Schema::new_map(
1414            shape_id!("test", "M"),
1415            &aws_smithy_schema::prelude::STRING,
1416            &aws_smithy_schema::prelude::STRING,
1417        );
1418        static OUTER: Schema<'static> =
1419            Schema::new_member(shape_id!("test", "I"), ShapeType::List, "Items", 0);
1420
1421        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1422        ser.write_list(&OUTER, &|s| {
1423            s.write_map(&MAP_SCHEMA, &|m| {
1424                m.write_string(&aws_smithy_schema::prelude::STRING, "k1")?;
1425                m.write_string(&aws_smithy_schema::prelude::STRING, "v1")
1426            })?;
1427            s.write_map(&MAP_SCHEMA, &|m| {
1428                m.write_string(&aws_smithy_schema::prelude::STRING, "k2")?;
1429                m.write_string(&aws_smithy_schema::prelude::STRING, "v2")
1430            })
1431        })
1432        .unwrap();
1433
1434        assert_eq!(
1435            String::from_utf8(ser.finish()).unwrap(),
1436            "Action=Op&Version=1.0\
1437             &Items.member.1.entry.1.key=k1&Items.member.1.entry.1.value=v1\
1438             &Items.member.2.entry.1.key=k2&Items.member.2.entry.1.value=v2",
1439        );
1440    }
1441
1442    #[test]
1443    fn float_whole_number_keeps_decimal() {
1444        static M: Schema<'static> =
1445            Schema::new_member(shape_id!("test", "I"), ShapeType::Float, "Val", 0);
1446        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1447        ser.write_double(&M, 5.0).unwrap();
1448        assert_eq!(
1449            String::from_utf8(ser.finish()).unwrap(),
1450            "Action=Op&Version=1.0&Val=5.0"
1451        );
1452    }
1453
1454    #[test]
1455    fn float_special_values() {
1456        static M: Schema<'static> =
1457            Schema::new_member(shape_id!("test", "I"), ShapeType::Float, "Val", 0);
1458        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1459        ser.write_float(&M, f32::INFINITY).unwrap();
1460        assert_eq!(
1461            String::from_utf8(ser.finish()).unwrap(),
1462            "Action=Op&Version=1.0&Val=Infinity"
1463        );
1464
1465        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1466        ser.write_float(&M, f32::NEG_INFINITY).unwrap();
1467        assert_eq!(
1468            String::from_utf8(ser.finish()).unwrap(),
1469            "Action=Op&Version=1.0&Val=-Infinity"
1470        );
1471
1472        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1473        ser.write_float(&M, f32::NAN).unwrap();
1474        assert_eq!(
1475            String::from_utf8(ser.finish()).unwrap(),
1476            "Action=Op&Version=1.0&Val=NaN"
1477        );
1478    }
1479
1480    #[test]
1481    fn double_special_values() {
1482        static M: Schema<'static> =
1483            Schema::new_member(shape_id!("test", "I"), ShapeType::Double, "Val", 0);
1484        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1485        ser.write_double(&M, f64::INFINITY).unwrap();
1486        assert_eq!(
1487            String::from_utf8(ser.finish()).unwrap(),
1488            "Action=Op&Version=1.0&Val=Infinity"
1489        );
1490
1491        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1492        ser.write_double(&M, f64::NEG_INFINITY).unwrap();
1493        assert_eq!(
1494            String::from_utf8(ser.finish()).unwrap(),
1495            "Action=Op&Version=1.0&Val=-Infinity"
1496        );
1497    }
1498
1499    #[test]
1500    fn blob_base64_encoded() {
1501        static M: Schema<'static> =
1502            Schema::new_member(shape_id!("test", "I"), ShapeType::Blob, "Data", 0);
1503        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1504        ser.write_blob(&M, aws_smithy_types::Blob::new(&b"hello"[..]))
1505            .unwrap();
1506        assert_eq!(
1507            String::from_utf8(ser.finish()).unwrap(),
1508            "Action=Op&Version=1.0&Data=aGVsbG8%3D"
1509        );
1510    }
1511
1512    #[test]
1513    fn document_returns_error() {
1514        static M: Schema<'static> =
1515            Schema::new_member(shape_id!("test", "I"), ShapeType::Document, "Doc", 0);
1516        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1517        let result = ser.write_document(&M, &Document::Null);
1518        assert!(result.is_err());
1519    }
1520
1521    #[test]
1522    fn write_null_is_noop() {
1523        static M: Schema<'static> =
1524            Schema::new_member(shape_id!("test", "I"), ShapeType::String, "Val", 0);
1525        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1526        ser.write_null(&M).unwrap();
1527        assert_eq!(
1528            String::from_utf8(ser.finish()).unwrap(),
1529            "Action=Op&Version=1.0"
1530        );
1531    }
1532
1533    #[test]
1534    fn numeric_types() {
1535        static B: Schema<'static> =
1536            Schema::new_member(shape_id!("t", "S"), ShapeType::Byte, "B", 0);
1537        static S: Schema<'static> =
1538            Schema::new_member(shape_id!("t", "S"), ShapeType::Short, "S", 0);
1539        static L: Schema<'static> =
1540            Schema::new_member(shape_id!("t", "S"), ShapeType::Long, "L", 0);
1541        let mut ser = QueryShapeSerializer::new("Op", "1.0");
1542        ser.write_byte(&B, -1).unwrap();
1543        ser.write_short(&S, 32000).unwrap();
1544        ser.write_long(&L, 9999999999).unwrap();
1545        assert_eq!(
1546            String::from_utf8(ser.finish()).unwrap(),
1547            "Action=Op&Version=1.0&B=-1&S=32000&L=9999999999"
1548        );
1549    }
1550}