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