aws_smithy_schema/schema/http_protocol/
binding.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! HTTP binding protocol for REST-style APIs.
7
8use crate::codec::{Codec, FinishSerializer};
9use crate::protocol::ClientProtocol;
10use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer, ShapeSerializer};
11use crate::{Schema, ShapeId};
12use aws_smithy_runtime_api::http::{Request, Response};
13use aws_smithy_types::body::SdkBody;
14use aws_smithy_types::config_bag::ConfigBag;
15
16/// An HTTP protocol for REST-style APIs that use HTTP bindings.
17///
18/// This protocol splits input members between HTTP locations (headers, query
19/// strings, URI labels) and the payload based on HTTP binding traits
20/// (`@httpHeader`, `@httpQuery`, `@httpLabel`, `@httpPayload`, etc.).
21/// Non-bound members are serialized into the body using the provided codec.
22///
23/// # Type parameters
24///
25/// * `C` — the payload codec (e.g., `JsonCodec`, `XmlCodec`)
26#[derive(Debug)]
27pub struct HttpBindingProtocol<C> {
28    protocol_id: ShapeId,
29    codec: C,
30    content_type: &'static str,
31}
32
33impl<C: Codec> HttpBindingProtocol<C> {
34    /// Creates a new HTTP binding protocol.
35    pub fn new(protocol_id: ShapeId, codec: C, content_type: &'static str) -> Self {
36        Self {
37            protocol_id,
38            codec,
39            content_type,
40        }
41    }
42}
43
44// Note: there is a percent_encoding crate we use some other places for this, but I'm trying to keep
45// the dependencies to a minimum.
46/// Percent-encode a string per RFC 3986 section 2.3 (unreserved characters only).
47pub fn percent_encode(input: &str) -> String {
48    let mut out = String::with_capacity(input.len());
49    for byte in input.bytes() {
50        match byte {
51            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
52                out.push(byte as char);
53            }
54            _ => {
55                out.push('%');
56                out.push(char::from(HEX[(byte >> 4) as usize]));
57                out.push(char::from(HEX[(byte & 0x0f) as usize]));
58            }
59        }
60    }
61    out
62}
63
64pub(crate) const HEX: &[u8; 16] = b"0123456789ABCDEF";
65
66/// A ShapeSerializer that intercepts member writes and routes HTTP-bound
67/// members to headers, query params, or URI labels instead of the body.
68///
69/// Members without HTTP binding traits are forwarded to the inner body
70/// serializer unchanged.
71struct HttpBindingSerializer<'a, S> {
72    body: S,
73    headers: Vec<(String, String)>,
74    query_params: Vec<(String, String)>,
75    labels: Vec<(String, String)>,
76    /// When set, member schemas are resolved from this schema by name to find
77    /// HTTP binding traits. This allows the protocol to override bindings
78    /// (e.g., for presigning where body members become query params).
79    input_schema: Option<&'a Schema>,
80    /// True for the top-level input struct in serialize_request.
81    /// Cleared after the first write_struct so nested structs delegate directly.
82    is_top_level: bool,
83    /// Raw payload bytes for `@httpPayload` blob/string members. When a member
84    /// has `@httpPayload` and targets a blob or string, the raw bytes bypass
85    /// the codec serializer entirely and are used as the HTTP body directly.
86    /// Safety: the referenced bytes are borrowed from the input struct passed to
87    /// `serialize_request`, which outlives this serializer.
88    raw_payload: Option<&'a [u8]>,
89}
90
91impl<'a, S> HttpBindingSerializer<'a, S> {
92    fn new(body: S, input_schema: Option<&'a Schema>) -> Self {
93        Self {
94            body,
95            headers: Vec::new(),
96            query_params: Vec::new(),
97            labels: Vec::new(),
98            input_schema,
99            is_top_level: true,
100            raw_payload: None,
101        }
102    }
103
104    /// Resolve the effective member schema: if an input_schema override is set,
105    /// look up the member by name there (to get the correct HTTP bindings).
106    /// Otherwise use the schema as-is.
107    fn resolve_member<'s>(&self, schema: &'s Schema) -> &'s Schema
108    where
109        'a: 's,
110    {
111        if let (Some(input_schema), Some(idx)) = (self.input_schema, schema.member_index()) {
112            input_schema.member_schema_by_index(idx).unwrap_or(schema)
113        } else if let (Some(input_schema), Some(name)) = (self.input_schema, schema.member_name()) {
114            // Fallback to name lookup for schemas without a member index
115            input_schema.member_schema(name).unwrap_or(schema)
116        } else {
117            schema
118        }
119    }
120}
121
122impl<'a, S: ShapeSerializer> ShapeSerializer for HttpBindingSerializer<'a, S> {
123    fn write_struct(
124        &mut self,
125        schema: &Schema,
126        value: &dyn SerializableStruct,
127    ) -> Result<(), SerdeError> {
128        if self.is_top_level {
129            // Top-level input struct: route serialize_members through the binder
130            // so HTTP-bound members are intercepted. The body serializer's
131            // write_struct is used for framing (e.g., { } for JSON), with a
132            // proxy whose serialize_members delegates back to the binder.
133            struct Proxy<'a, 'b, S> {
134                binder: &'a mut HttpBindingSerializer<'b, S>,
135                value: &'a dyn SerializableStruct,
136            }
137            impl<S: ShapeSerializer> SerializableStruct for Proxy<'_, '_, S> {
138                fn serialize_members(
139                    &self,
140                    _serializer: &mut dyn ShapeSerializer,
141                ) -> Result<(), SerdeError> {
142                    let binder = self.binder as *const HttpBindingSerializer<'_, S>
143                        as *mut HttpBindingSerializer<'_, S>;
144                    // SAFETY: The body serializer called serialize_members on
145                    // this proxy, passing &mut self (body). The binder wraps
146                    // that same body serializer. We need mutable access to the
147                    // binder to route writes. This is safe because:
148                    // 1. The body serializer's write_struct only calls
149                    //    serialize_members once, synchronously.
150                    // 2. Body member writes from the binder go back to the
151                    //    body serializer, which is in a valid state (between
152                    //    the { and } it emitted).
153                    self.value.serialize_members(unsafe { &mut *binder })
154                }
155            }
156            // Clear is_top_level so nested write_struct calls (from body members)
157            // take the else branch and delegate directly to the body serializer.
158            // input_schema is preserved so resolve_member continues to work.
159            self.is_top_level = false;
160            let proxy = Proxy {
161                binder: self,
162                value,
163            };
164            let binder_ptr = &mut *proxy.binder as *mut HttpBindingSerializer<'_, S>;
165            // SAFETY: `proxy` holds a shared reference to `binder` (via &mut that
166            // we reborrow). We need to call `binder.body.write_struct(schema, &proxy)`
167            // but can't do so through normal references because `proxy` borrows `binder`.
168            // The raw pointer dereference is safe because:
169            // 1. `binder_ptr` points to a valid, live `HttpBindingSerializer` (it was
170            //    just derived from `proxy.binder`).
171            // 2. `body.write_struct` is called synchronously and returns before `proxy`
172            //    is dropped, so the binder is not moved or deallocated.
173            // 3. The only re-entrant access is through `proxy.serialize_members`, which
174            //    uses the same raw-pointer pattern with its own safety justification above.
175            unsafe { (*binder_ptr).body.write_struct(schema, &proxy) }
176        } else {
177            // Nested struct (a body member targeting a structure): delegate
178            // entirely to the body serializer.
179            let schema = self.resolve_member(schema);
180            if schema.http_payload().is_some() {
181                // @httpPayload struct/union: write as the body's top-level object
182                // without a member name prefix. Use a non-member schema for the
183                // write_struct call so prefix() doesn't emit a field name.
184                self.body.write_struct(&crate::prelude::DOCUMENT, value)?;
185                return Ok(());
186            }
187            self.body.write_struct(schema, value)
188        }
189    }
190
191    fn write_list(
192        &mut self,
193        schema: &Schema,
194        write_elements: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
195    ) -> Result<(), SerdeError> {
196        let schema = self.resolve_member(schema);
197        // @httpHeader on a list: collect elements as comma-separated header value
198        if let Some(header) = schema.http_header() {
199            let mut collector = ListElementCollector::for_header();
200            write_elements(&mut collector)?;
201            // RFC 7230: string values containing commas or quotes need quoting.
202            // Timestamps are NOT quoted even though http-date contains commas.
203            let header_val = collector
204                .values
205                .iter()
206                .zip(collector.quotable.iter())
207                .map(|(s, &quotable)| {
208                    if quotable && (s.contains(',') || s.contains('"')) {
209                        format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
210                    } else {
211                        s.clone()
212                    }
213                })
214                .collect::<Vec<_>>()
215                .join(", ");
216            self.headers.push((header.value().to_string(), header_val));
217            return Ok(());
218        }
219        // @httpQuery on a list: add each element as a separate query param
220        if let Some(query) = schema.http_query() {
221            let mut collector = ListElementCollector::for_query();
222            write_elements(&mut collector)?;
223            for val in collector.values {
224                self.query_params.push((query.value().to_string(), val));
225            }
226            return Ok(());
227        }
228        self.body.write_list(schema, write_elements)
229    }
230
231    fn write_map(
232        &mut self,
233        schema: &Schema,
234        write_entries: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
235    ) -> Result<(), SerdeError> {
236        let schema = self.resolve_member(schema);
237        // @httpPrefixHeaders: serialize map entries as prefixed headers
238        if let Some(prefix) = schema.http_prefix_headers() {
239            // Collect entries via a temporary serializer
240            let mut collector = MapEntryCollector::new(prefix.value().to_string());
241            write_entries(&mut collector)?;
242            self.headers.extend(collector.entries);
243            return Ok(());
244        }
245        // @httpQueryParams: serialize map entries as query params
246        if schema.http_query_params().is_some() {
247            let mut collector = MapEntryCollector::new(String::new());
248            write_entries(&mut collector)?;
249            // Filter out keys that overlap with explicit @httpQuery params
250            // (query params take precedence over query params map entries)
251            let explicit_query_keys: Vec<&str> = self
252                .input_schema
253                .map(|s| {
254                    s.members()
255                        .iter()
256                        .filter_map(|m| m.http_query().map(|q| q.value()))
257                        .collect()
258                })
259                .unwrap_or_default();
260            for (k, v) in collector.entries {
261                if !explicit_query_keys.contains(&k.as_str()) {
262                    self.query_params.push((k, v));
263                }
264            }
265            return Ok(());
266        }
267        self.body.write_map(schema, write_entries)
268    }
269
270    fn write_boolean(&mut self, schema: &Schema, value: bool) -> Result<(), SerdeError> {
271        let schema = self.resolve_member(schema);
272        if let Some(binding) = http_string_binding(schema) {
273            return self.add_binding(binding, schema, &value.to_string());
274        }
275        self.body.write_boolean(schema, value)
276    }
277
278    fn write_byte(&mut self, schema: &Schema, value: i8) -> Result<(), SerdeError> {
279        let schema = self.resolve_member(schema);
280        if let Some(binding) = http_string_binding(schema) {
281            return self.add_binding(binding, schema, &value.to_string());
282        }
283        self.body.write_byte(schema, value)
284    }
285
286    fn write_short(&mut self, schema: &Schema, value: i16) -> Result<(), SerdeError> {
287        let schema = self.resolve_member(schema);
288        if let Some(binding) = http_string_binding(schema) {
289            return self.add_binding(binding, schema, &value.to_string());
290        }
291        self.body.write_short(schema, value)
292    }
293
294    fn write_integer(&mut self, schema: &Schema, value: i32) -> Result<(), SerdeError> {
295        let schema = self.resolve_member(schema);
296        if let Some(binding) = http_string_binding(schema) {
297            return self.add_binding(binding, schema, &value.to_string());
298        }
299        self.body.write_integer(schema, value)
300    }
301
302    fn write_long(&mut self, schema: &Schema, value: i64) -> Result<(), SerdeError> {
303        let schema = self.resolve_member(schema);
304        if let Some(binding) = http_string_binding(schema) {
305            return self.add_binding(binding, schema, &value.to_string());
306        }
307        self.body.write_long(schema, value)
308    }
309
310    fn write_float(&mut self, schema: &Schema, value: f32) -> Result<(), SerdeError> {
311        let schema = self.resolve_member(schema);
312        if let Some(binding) = http_string_binding(schema) {
313            return self.add_binding(binding, schema, &format_float_f32(value));
314        }
315        self.body.write_float(schema, value)
316    }
317
318    fn write_double(&mut self, schema: &Schema, value: f64) -> Result<(), SerdeError> {
319        let schema = self.resolve_member(schema);
320        if let Some(binding) = http_string_binding(schema) {
321            return self.add_binding(binding, schema, &format_float_f64(value));
322        }
323        self.body.write_double(schema, value)
324    }
325
326    fn write_big_integer(
327        &mut self,
328        schema: &Schema,
329        value: &aws_smithy_types::BigInteger,
330    ) -> Result<(), SerdeError> {
331        let schema = self.resolve_member(schema);
332        if let Some(binding) = http_string_binding(schema) {
333            return self.add_binding(binding, schema, value.as_ref());
334        }
335        self.body.write_big_integer(schema, value)
336    }
337
338    fn write_big_decimal(
339        &mut self,
340        schema: &Schema,
341        value: &aws_smithy_types::BigDecimal,
342    ) -> Result<(), SerdeError> {
343        let schema = self.resolve_member(schema);
344        if let Some(binding) = http_string_binding(schema) {
345            return self.add_binding(binding, schema, value.as_ref());
346        }
347        self.body.write_big_decimal(schema, value)
348    }
349
350    fn write_string(&mut self, schema: &Schema, value: &str) -> Result<(), SerdeError> {
351        let schema = self.resolve_member(schema);
352        if let Some(binding) = http_string_binding(schema) {
353            // @mediaType on a header: base64-encode the value
354            if schema.media_type().is_some() {
355                let encoded = aws_smithy_types::base64::encode(value.as_bytes());
356                return self.add_binding(binding, schema, &encoded);
357            }
358            return self.add_binding(binding, schema, value);
359        }
360        if schema.http_payload().is_some() {
361            // SAFETY: We extend the lifetime of `value.as_bytes()` from its anonymous
362            // lifetime to `'a`. This is sound because:
363            // 1. `value` is borrowed from the input struct passed to `serialize_request`.
364            // 2. `HttpBindingSerializer` is a local variable within `serialize_request`
365            //    and is dropped before `serialize_request` returns.
366            // 3. The input struct (and thus `value`) outlives the serializer.
367            // 4. `raw_payload` is read in `serialize_request` immediately after
368            //    `serialize_members` returns, before the input is dropped.
369            // We use transmute rather than copying to avoid allocating for potentially
370            // multi-GB string payloads.
371            self.raw_payload =
372                Some(unsafe { std::mem::transmute::<&[u8], &'a [u8]>(value.as_bytes()) });
373            return Ok(());
374        }
375        self.body.write_string(schema, value)
376    }
377
378    fn write_blob(
379        &mut self,
380        schema: &Schema,
381        value: &aws_smithy_types::Blob,
382    ) -> Result<(), SerdeError> {
383        let schema = self.resolve_member(schema);
384        if schema.http_header().is_some() {
385            let encoded = aws_smithy_types::base64::encode(value.as_ref());
386            self.headers
387                .push((schema.http_header().unwrap().value().to_string(), encoded));
388            return Ok(());
389        }
390        if schema.http_payload().is_some() {
391            // SAFETY: We extend the lifetime of `value.as_ref()` (a `&[u8]`) from its
392            // anonymous lifetime to `'a`. This is sound because:
393            // 1. `value` is borrowed from the input struct passed to `serialize_request`.
394            // 2. `HttpBindingSerializer` is a local variable within `serialize_request`
395            //    and is dropped before `serialize_request` returns.
396            // 3. The input struct (and thus `value`) outlives the serializer.
397            // 4. `raw_payload` is read in `serialize_request` immediately after
398            //    `serialize_members` returns, before the input is dropped.
399            // We use transmute rather than copying to avoid allocating for potentially
400            // multi-GB blob payloads.
401            self.raw_payload =
402                Some(unsafe { std::mem::transmute::<&[u8], &'a [u8]>(value.as_ref()) });
403            return Ok(());
404        }
405        self.body.write_blob(schema, value)
406    }
407
408    fn write_timestamp(
409        &mut self,
410        schema: &Schema,
411        value: &aws_smithy_types::DateTime,
412    ) -> Result<(), SerdeError> {
413        let schema = self.resolve_member(schema);
414        if let Some(binding) = http_string_binding(schema) {
415            // Headers default to http-date, query/label default to date-time
416            let format = if let Some(ts_trait) = schema.timestamp_format() {
417                match ts_trait.format() {
418                    crate::traits::TimestampFormat::EpochSeconds => {
419                        aws_smithy_types::date_time::Format::EpochSeconds
420                    }
421                    crate::traits::TimestampFormat::HttpDate => {
422                        aws_smithy_types::date_time::Format::HttpDate
423                    }
424                    crate::traits::TimestampFormat::DateTime => {
425                        aws_smithy_types::date_time::Format::DateTime
426                    }
427                }
428            } else {
429                match binding {
430                    HttpBinding::Header(_) => aws_smithy_types::date_time::Format::HttpDate,
431                    _ => aws_smithy_types::date_time::Format::DateTime,
432                }
433            };
434            let formatted = value
435                .fmt(format)
436                .map_err(|e| SerdeError::custom(format!("failed to format timestamp: {e}")))?;
437            return self.add_binding(binding, schema, &formatted);
438        }
439        self.body.write_timestamp(schema, value)
440    }
441
442    fn write_document(
443        &mut self,
444        schema: &Schema,
445        value: &aws_smithy_types::Document,
446    ) -> Result<(), SerdeError> {
447        self.body.write_document(schema, value)
448    }
449
450    fn write_null(&mut self, schema: &Schema) -> Result<(), SerdeError> {
451        self.body.write_null(schema)
452    }
453}
454
455/// Which HTTP location a member is bound to.
456enum HttpBinding<'a> {
457    Header(&'a str),
458    Query(&'a str),
459    Label,
460}
461
462/// Determine the HTTP binding for a member schema, if any.
463fn http_string_binding(schema: &Schema) -> Option<HttpBinding<'_>> {
464    if let Some(h) = schema.http_header() {
465        return Some(HttpBinding::Header(h.value()));
466    }
467    if let Some(q) = schema.http_query() {
468        return Some(HttpBinding::Query(q.value()));
469    }
470    if schema.http_label().is_some() {
471        return Some(HttpBinding::Label);
472    }
473    None
474}
475
476impl<'a, S> HttpBindingSerializer<'a, S> {
477    fn add_binding(
478        &mut self,
479        binding: HttpBinding<'_>,
480        schema: &Schema,
481        value: &str,
482    ) -> Result<(), SerdeError> {
483        match binding {
484            HttpBinding::Header(name) => {
485                self.headers.push((name.to_string(), value.to_string()));
486            }
487            HttpBinding::Query(name) => {
488                self.query_params
489                    .push((name.to_string(), value.to_string()));
490            }
491            HttpBinding::Label => {
492                let name = schema
493                    .member_name()
494                    .ok_or_else(|| SerdeError::custom("httpLabel on non-member schema"))?;
495                self.labels.push((name.to_string(), value.to_string()));
496            }
497        }
498        Ok(())
499    }
500}
501
502/// Collects list element values as strings for @httpHeader and @httpQuery on lists.
503struct ListElementCollector {
504    values: Vec<String>,
505    /// Whether each value should be quoted if it contains commas (strings yes, timestamps no)
506    quotable: Vec<bool>,
507    /// Whether this collector is for a header (true) or query param (false).
508    /// Affects default timestamp format: http-date for headers, date-time for query.
509    is_header: bool,
510}
511
512impl ListElementCollector {
513    fn for_header() -> Self {
514        Self::new(true)
515    }
516
517    fn for_query() -> Self {
518        Self::new(false)
519    }
520
521    fn new(is_header: bool) -> Self {
522        Self {
523            values: Vec::new(),
524            quotable: Vec::new(),
525            is_header,
526        }
527    }
528
529    fn push(&mut self, value: String) {
530        self.quotable.push(true);
531        self.values.push(value);
532    }
533
534    fn push_unquotable(&mut self, value: String) {
535        self.quotable.push(false);
536        self.values.push(value);
537    }
538}
539
540impl ShapeSerializer for ListElementCollector {
541    fn write_string(&mut self, _schema: &Schema, value: &str) -> Result<(), SerdeError> {
542        self.push(value.to_string());
543        Ok(())
544    }
545    fn write_boolean(&mut self, _: &Schema, value: bool) -> Result<(), SerdeError> {
546        self.push(value.to_string());
547        Ok(())
548    }
549    fn write_byte(&mut self, _: &Schema, value: i8) -> Result<(), SerdeError> {
550        self.push(value.to_string());
551        Ok(())
552    }
553    fn write_short(&mut self, _: &Schema, value: i16) -> Result<(), SerdeError> {
554        self.push(value.to_string());
555        Ok(())
556    }
557    fn write_integer(&mut self, _: &Schema, value: i32) -> Result<(), SerdeError> {
558        self.push(value.to_string());
559        Ok(())
560    }
561    fn write_long(&mut self, _: &Schema, value: i64) -> Result<(), SerdeError> {
562        self.push(value.to_string());
563        Ok(())
564    }
565    fn write_float(&mut self, _: &Schema, value: f32) -> Result<(), SerdeError> {
566        self.push(format_float_f32(value));
567        Ok(())
568    }
569    fn write_double(&mut self, _: &Schema, value: f64) -> Result<(), SerdeError> {
570        self.push(format_float_f64(value));
571        Ok(())
572    }
573    fn write_timestamp(
574        &mut self,
575        schema: &Schema,
576        value: &aws_smithy_types::DateTime,
577    ) -> Result<(), SerdeError> {
578        let format = match schema.timestamp_format() {
579            Some(ts) => match ts.format() {
580                crate::traits::TimestampFormat::EpochSeconds => {
581                    aws_smithy_types::date_time::Format::EpochSeconds
582                }
583                crate::traits::TimestampFormat::HttpDate => {
584                    aws_smithy_types::date_time::Format::HttpDate
585                }
586                crate::traits::TimestampFormat::DateTime => {
587                    aws_smithy_types::date_time::Format::DateTime
588                }
589            },
590            // Default: headers use http-date, query params use date-time
591            None => {
592                if self.is_header {
593                    aws_smithy_types::date_time::Format::HttpDate
594                } else {
595                    aws_smithy_types::date_time::Format::DateTime
596                }
597            }
598        };
599        self.push_unquotable(
600            value
601                .fmt(format)
602                .map_err(|e| SerdeError::custom(format!("failed to format timestamp: {e}")))?,
603        );
604        Ok(())
605    }
606    fn write_blob(
607        &mut self,
608        _schema: &Schema,
609        value: &aws_smithy_types::Blob,
610    ) -> Result<(), SerdeError> {
611        self.push(aws_smithy_types::base64::encode(value.as_ref()));
612        Ok(())
613    }
614    // Remaining methods are no-ops for list element collection
615    fn write_struct(&mut self, _: &Schema, _: &dyn SerializableStruct) -> Result<(), SerdeError> {
616        Ok(())
617    }
618    fn write_list(
619        &mut self,
620        _: &Schema,
621        _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
622    ) -> Result<(), SerdeError> {
623        Ok(())
624    }
625    fn write_map(
626        &mut self,
627        _: &Schema,
628        _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
629    ) -> Result<(), SerdeError> {
630        Ok(())
631    }
632    fn write_big_integer(
633        &mut self,
634        _: &Schema,
635        _: &aws_smithy_types::BigInteger,
636    ) -> Result<(), SerdeError> {
637        Ok(())
638    }
639    fn write_big_decimal(
640        &mut self,
641        _: &Schema,
642        _: &aws_smithy_types::BigDecimal,
643    ) -> Result<(), SerdeError> {
644        Ok(())
645    }
646    fn write_document(
647        &mut self,
648        _: &Schema,
649        _: &aws_smithy_types::Document,
650    ) -> Result<(), SerdeError> {
651        Ok(())
652    }
653    fn write_null(&mut self, _: &Schema) -> Result<(), SerdeError> {
654        Ok(())
655    }
656}
657
658/// Format a float for HTTP headers/query/labels.
659/// Rust's Display writes "inf"/"-inf" but HTTP requires "Infinity"/"-Infinity".
660fn format_float_f32(value: f32) -> String {
661    if value.is_infinite() {
662        if value.is_sign_positive() {
663            "Infinity".to_string()
664        } else {
665            "-Infinity".to_string()
666        }
667    } else if value.is_nan() {
668        "NaN".to_string()
669    } else {
670        value.to_string()
671    }
672}
673
674fn format_float_f64(value: f64) -> String {
675    if value.is_infinite() {
676        if value.is_sign_positive() {
677            "Infinity".to_string()
678        } else {
679            "-Infinity".to_string()
680        }
681    } else if value.is_nan() {
682        "NaN".to_string()
683    } else {
684        value.to_string()
685    }
686}
687
688/// Collects map key-value pairs written via ShapeSerializer for
689/// @httpPrefixHeaders and @httpQueryParams.
690struct MapEntryCollector {
691    prefix: String,
692    entries: Vec<(String, String)>,
693    pending_key: Option<String>,
694}
695
696impl MapEntryCollector {
697    fn new(prefix: String) -> Self {
698        Self {
699            prefix,
700            entries: Vec::new(),
701            pending_key: None,
702        }
703    }
704}
705
706impl ShapeSerializer for MapEntryCollector {
707    fn write_string(&mut self, _schema: &Schema, value: &str) -> Result<(), SerdeError> {
708        if let Some(key) = self.pending_key.take() {
709            self.entries
710                .push((format!("{}{}", self.prefix, key), value.to_string()));
711        } else {
712            self.pending_key = Some(value.to_string());
713        }
714        Ok(())
715    }
716
717    // All other methods are no-ops — maps in HTTP bindings only have string keys/values.
718    // Exception: write_list handles Map<String, List<String>> for @httpQueryParams.
719    fn write_struct(&mut self, _: &Schema, _: &dyn SerializableStruct) -> Result<(), SerdeError> {
720        Ok(())
721    }
722    fn write_list(
723        &mut self,
724        _: &Schema,
725        write_elements: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
726    ) -> Result<(), SerdeError> {
727        // Map<String, List<String>>: each list element becomes a separate entry
728        // with the same key (for @httpQueryParams).
729        if let Some(key) = self.pending_key.take() {
730            let mut collector = ListElementCollector::for_query(); // query params context
731            write_elements(&mut collector)?;
732            for val in collector.values {
733                self.entries.push((format!("{}{}", self.prefix, key), val));
734            }
735        }
736        Ok(())
737    }
738    fn write_map(
739        &mut self,
740        _: &Schema,
741        _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
742    ) -> Result<(), SerdeError> {
743        Ok(())
744    }
745    fn write_boolean(&mut self, _: &Schema, _: bool) -> Result<(), SerdeError> {
746        Ok(())
747    }
748    fn write_byte(&mut self, _: &Schema, _: i8) -> Result<(), SerdeError> {
749        Ok(())
750    }
751    fn write_short(&mut self, _: &Schema, _: i16) -> Result<(), SerdeError> {
752        Ok(())
753    }
754    fn write_integer(&mut self, _: &Schema, _: i32) -> Result<(), SerdeError> {
755        Ok(())
756    }
757    fn write_long(&mut self, _: &Schema, _: i64) -> Result<(), SerdeError> {
758        Ok(())
759    }
760    fn write_float(&mut self, _: &Schema, _: f32) -> Result<(), SerdeError> {
761        Ok(())
762    }
763    fn write_double(&mut self, _: &Schema, _: f64) -> Result<(), SerdeError> {
764        Ok(())
765    }
766    fn write_big_integer(
767        &mut self,
768        _: &Schema,
769        _: &aws_smithy_types::BigInteger,
770    ) -> Result<(), SerdeError> {
771        Ok(())
772    }
773    fn write_big_decimal(
774        &mut self,
775        _: &Schema,
776        _: &aws_smithy_types::BigDecimal,
777    ) -> Result<(), SerdeError> {
778        Ok(())
779    }
780    fn write_blob(&mut self, _: &Schema, _: &aws_smithy_types::Blob) -> Result<(), SerdeError> {
781        Ok(())
782    }
783    fn write_timestamp(
784        &mut self,
785        _: &Schema,
786        _: &aws_smithy_types::DateTime,
787    ) -> Result<(), SerdeError> {
788        Ok(())
789    }
790    fn write_document(
791        &mut self,
792        _: &Schema,
793        _: &aws_smithy_types::Document,
794    ) -> Result<(), SerdeError> {
795        Ok(())
796    }
797    fn write_null(&mut self, _: &Schema) -> Result<(), SerdeError> {
798        Ok(())
799    }
800}
801
802impl<C> ClientProtocol for HttpBindingProtocol<C>
803where
804    C: Codec + Send + Sync + std::fmt::Debug + 'static,
805    for<'a> C::Deserializer<'a>: ShapeDeserializer,
806{
807    fn protocol_id(&self) -> &ShapeId {
808        &self.protocol_id
809    }
810
811    fn serialize_request(
812        &self,
813        input: &dyn SerializableStruct,
814        input_schema: &Schema,
815        endpoint: &str,
816        _cfg: &ConfigBag,
817    ) -> Result<Request, SerdeError> {
818        let mut binder =
819            HttpBindingSerializer::new(self.codec.create_serializer(), Some(input_schema));
820
821        // Check if there's an @httpPayload member targeting a structure/union.
822        // In that case, the payload member's own write_struct provides the body
823        // framing, so we must not add top-level struct framing.
824        let has_struct_payload = input_schema.members().iter().any(|m| {
825            m.http_payload().is_some()
826                && matches!(
827                    m.shape_type(),
828                    crate::ShapeType::Structure | crate::ShapeType::Union
829                )
830        });
831        if has_struct_payload {
832            binder.is_top_level = false;
833            input.serialize_members(&mut binder)?;
834        } else {
835            binder.write_struct(input_schema, input)?;
836        }
837        let raw_payload = binder.raw_payload;
838        let mut body = if raw_payload.is_some() {
839            // @httpPayload blob/string — don't use the codec output
840            Vec::new()
841        } else {
842            binder.body.finish()
843        };
844
845        // Per the REST-JSON content-type handling spec:
846        // - If @httpPayload targets a blob/string: send raw bytes, no Content-Type when empty
847        // - If body members exist (even if all optional and unset): send `{}` with Content-Type
848        // - If no body members at all (everything is in headers/query/labels): empty body, no Content-Type
849        let has_blob_or_string_payload = raw_payload.is_some();
850        let has_body_members = has_struct_payload
851            || input_schema.members().iter().any(|m| {
852                m.http_header().is_none()
853                    && m.http_query().is_none()
854                    && m.http_label().is_none()
855                    && m.http_prefix_headers().is_none()
856                    && m.http_query_params().is_none()
857                    && m.http_payload().is_none()
858            });
859
860        let set_content_type = if has_blob_or_string_payload {
861            // Blob/string payload: Content-Type comes from the @httpHeader("Content-Type")
862            // member if present, or defaults to application/octet-stream for blobs.
863            // Don't set the protocol's codec content type (e.g., application/json).
864            false
865        } else if has_body_members {
866            // Operation has body members — body includes framing (e.g., `{}`).
867            // Per the REST-JSON spec, even if all members are optional and unset, send `{}`.
868            true
869        } else {
870            // No body members at all — empty body, no Content-Type.
871            body = Vec::new();
872            false
873        };
874
875        // Build URI: use @http trait if available (with label substitution from binder),
876        // otherwise fall back to endpoint with manual label substitution.
877        let mut uri = match input_schema.http() {
878            Some(h) => {
879                let mut path = h.uri().to_string();
880                for (name, value) in &binder.labels {
881                    // Try greedy label first ({name+}), then regular ({name})
882                    let greedy = format!("{{{name}+}}");
883                    if path.contains(&greedy) {
884                        // Greedy labels: encode each path segment separately, preserve /
885                        let encoded = value
886                            .split('/')
887                            .map(|seg| percent_encode(seg))
888                            .collect::<Vec<_>>()
889                            .join("/");
890                        path = path.replace(&greedy, &encoded);
891                    } else {
892                        let placeholder = format!("{{{name}}}");
893                        path = path.replace(&placeholder, &percent_encode(value));
894                    }
895                }
896                if endpoint.is_empty() {
897                    path
898                } else {
899                    format!("{}{}", endpoint, path)
900                }
901            }
902            None => {
903                let mut u = if endpoint.is_empty() {
904                    "/".to_string()
905                } else {
906                    endpoint.to_string()
907                };
908                for (name, value) in &binder.labels {
909                    let greedy = format!("{{{name}+}}");
910                    if u.contains(&greedy) {
911                        let encoded = value
912                            .split('/')
913                            .map(|seg| percent_encode(seg))
914                            .collect::<Vec<_>>()
915                            .join("/");
916                        u = u.replace(&greedy, &encoded);
917                    } else {
918                        let placeholder = format!("{{{name}}}");
919                        u = u.replace(&placeholder, &percent_encode(value));
920                    }
921                }
922                u
923            }
924        };
925        if !binder.query_params.is_empty() {
926            uri.push(if uri.contains('?') { '&' } else { '?' });
927            let pairs: Vec<String> = binder
928                .query_params
929                .iter()
930                .map(|(k, v)| format!("{}={}", percent_encode(k), percent_encode(v)))
931                .collect();
932            uri.push_str(&pairs.join("&"));
933        }
934
935        let mut request = if let Some(payload) = raw_payload {
936            Request::new(SdkBody::from(payload))
937        } else {
938            Request::new(SdkBody::from(body))
939        };
940        // Set HTTP method from @http trait
941        if let Some(http) = input_schema.http() {
942            request
943                .set_method(http.method())
944                .map_err(|e| SerdeError::custom(format!("invalid HTTP method: {e}")))?;
945        }
946        request
947            .set_uri(uri.as_str())
948            .map_err(|e| SerdeError::custom(format!("invalid endpoint URI: {e}")))?;
949        if set_content_type {
950            request
951                .headers_mut()
952                .insert("Content-Type", self.content_type);
953        }
954        if let Some(len) = request.body().content_length() {
955            if len > 0 || set_content_type {
956                request
957                    .headers_mut()
958                    .insert("Content-Length", len.to_string());
959            }
960        }
961        for (name, value) in &binder.headers {
962            request.headers_mut().insert(name.clone(), value.clone());
963        }
964        Ok(request)
965    }
966
967    fn deserialize_response<'a>(
968        &self,
969        response: &'a Response,
970        _output_schema: &Schema,
971        _cfg: &ConfigBag,
972    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
973        let body = response
974            .body()
975            .bytes()
976            .ok_or_else(|| SerdeError::custom("response body is not available as bytes"))?;
977        Ok(Box::new(self.codec.create_deserializer(body)))
978    }
979}
980
981#[cfg(test)]
982mod tests {
983    use super::*;
984    use crate::serde::SerializableStruct;
985    use crate::{prelude::*, ShapeType};
986
987    struct TestSerializer {
988        output: Vec<u8>,
989    }
990
991    impl FinishSerializer for TestSerializer {
992        fn finish(self) -> Vec<u8> {
993            self.output
994        }
995    }
996
997    impl ShapeSerializer for TestSerializer {
998        fn write_struct(
999            &mut self,
1000            _: &Schema,
1001            value: &dyn SerializableStruct,
1002        ) -> Result<(), SerdeError> {
1003            self.output.push(b'{');
1004            value.serialize_members(self)?;
1005            self.output.push(b'}');
1006            Ok(())
1007        }
1008        fn write_list(
1009            &mut self,
1010            _: &Schema,
1011            _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
1012        ) -> Result<(), SerdeError> {
1013            Ok(())
1014        }
1015        fn write_map(
1016            &mut self,
1017            _: &Schema,
1018            _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
1019        ) -> Result<(), SerdeError> {
1020            Ok(())
1021        }
1022        fn write_boolean(&mut self, _: &Schema, _: bool) -> Result<(), SerdeError> {
1023            Ok(())
1024        }
1025        fn write_byte(&mut self, _: &Schema, _: i8) -> Result<(), SerdeError> {
1026            Ok(())
1027        }
1028        fn write_short(&mut self, _: &Schema, _: i16) -> Result<(), SerdeError> {
1029            Ok(())
1030        }
1031        fn write_integer(&mut self, _: &Schema, _: i32) -> Result<(), SerdeError> {
1032            Ok(())
1033        }
1034        fn write_long(&mut self, _: &Schema, _: i64) -> Result<(), SerdeError> {
1035            Ok(())
1036        }
1037        fn write_float(&mut self, _: &Schema, _: f32) -> Result<(), SerdeError> {
1038            Ok(())
1039        }
1040        fn write_double(&mut self, _: &Schema, _: f64) -> Result<(), SerdeError> {
1041            Ok(())
1042        }
1043        fn write_big_integer(
1044            &mut self,
1045            _: &Schema,
1046            _: &aws_smithy_types::BigInteger,
1047        ) -> Result<(), SerdeError> {
1048            Ok(())
1049        }
1050        fn write_big_decimal(
1051            &mut self,
1052            _: &Schema,
1053            _: &aws_smithy_types::BigDecimal,
1054        ) -> Result<(), SerdeError> {
1055            Ok(())
1056        }
1057        fn write_string(&mut self, _: &Schema, v: &str) -> Result<(), SerdeError> {
1058            self.output.extend_from_slice(v.as_bytes());
1059            Ok(())
1060        }
1061        fn write_blob(&mut self, _: &Schema, _: &aws_smithy_types::Blob) -> Result<(), SerdeError> {
1062            Ok(())
1063        }
1064        fn write_timestamp(
1065            &mut self,
1066            _: &Schema,
1067            _: &aws_smithy_types::DateTime,
1068        ) -> Result<(), SerdeError> {
1069            Ok(())
1070        }
1071        fn write_document(
1072            &mut self,
1073            _: &Schema,
1074            _: &aws_smithy_types::Document,
1075        ) -> Result<(), SerdeError> {
1076            Ok(())
1077        }
1078        fn write_null(&mut self, _: &Schema) -> Result<(), SerdeError> {
1079            Ok(())
1080        }
1081    }
1082
1083    struct TestDeserializer<'a> {
1084        input: &'a [u8],
1085    }
1086
1087    impl ShapeDeserializer for TestDeserializer<'_> {
1088        fn read_struct(
1089            &mut self,
1090            _: &Schema,
1091            _: &mut dyn FnMut(&Schema, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
1092        ) -> Result<(), SerdeError> {
1093            Ok(())
1094        }
1095        fn read_list(
1096            &mut self,
1097            _: &Schema,
1098            _: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
1099        ) -> Result<(), SerdeError> {
1100            Ok(())
1101        }
1102        fn read_map(
1103            &mut self,
1104            _: &Schema,
1105            _: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
1106        ) -> Result<(), SerdeError> {
1107            Ok(())
1108        }
1109        fn read_boolean(&mut self, _: &Schema) -> Result<bool, SerdeError> {
1110            Ok(false)
1111        }
1112        fn read_byte(&mut self, _: &Schema) -> Result<i8, SerdeError> {
1113            Ok(0)
1114        }
1115        fn read_short(&mut self, _: &Schema) -> Result<i16, SerdeError> {
1116            Ok(0)
1117        }
1118        fn read_integer(&mut self, _: &Schema) -> Result<i32, SerdeError> {
1119            Ok(0)
1120        }
1121        fn read_long(&mut self, _: &Schema) -> Result<i64, SerdeError> {
1122            Ok(0)
1123        }
1124        fn read_float(&mut self, _: &Schema) -> Result<f32, SerdeError> {
1125            Ok(0.0)
1126        }
1127        fn read_double(&mut self, _: &Schema) -> Result<f64, SerdeError> {
1128            Ok(0.0)
1129        }
1130        fn read_big_integer(
1131            &mut self,
1132            _: &Schema,
1133        ) -> Result<aws_smithy_types::BigInteger, SerdeError> {
1134            use std::str::FromStr;
1135            Ok(aws_smithy_types::BigInteger::from_str("0").unwrap())
1136        }
1137        fn read_big_decimal(
1138            &mut self,
1139            _: &Schema,
1140        ) -> Result<aws_smithy_types::BigDecimal, SerdeError> {
1141            use std::str::FromStr;
1142            Ok(aws_smithy_types::BigDecimal::from_str("0").unwrap())
1143        }
1144        fn read_string(&mut self, _: &Schema) -> Result<String, SerdeError> {
1145            Ok(String::from_utf8_lossy(self.input).into_owned())
1146        }
1147        fn read_blob(&mut self, _: &Schema) -> Result<aws_smithy_types::Blob, SerdeError> {
1148            Ok(aws_smithy_types::Blob::new(vec![]))
1149        }
1150        fn read_timestamp(&mut self, _: &Schema) -> Result<aws_smithy_types::DateTime, SerdeError> {
1151            Ok(aws_smithy_types::DateTime::from_secs(0))
1152        }
1153        fn read_document(&mut self, _: &Schema) -> Result<aws_smithy_types::Document, SerdeError> {
1154            Ok(aws_smithy_types::Document::Null)
1155        }
1156        fn is_null(&self) -> bool {
1157            false
1158        }
1159        fn container_size(&self) -> Option<usize> {
1160            None
1161        }
1162    }
1163
1164    #[derive(Debug)]
1165    struct TestCodec;
1166
1167    impl Codec for TestCodec {
1168        type Serializer = TestSerializer;
1169        type Deserializer<'a> = TestDeserializer<'a>;
1170        fn create_serializer(&self) -> Self::Serializer {
1171            TestSerializer { output: Vec::new() }
1172        }
1173        fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
1174            TestDeserializer { input }
1175        }
1176    }
1177
1178    static TEST_SCHEMA: Schema =
1179        Schema::new(crate::shape_id!("test", "TestStruct"), ShapeType::Structure);
1180
1181    struct EmptyStruct;
1182    impl SerializableStruct for EmptyStruct {
1183        fn serialize_members(&self, _: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1184            Ok(())
1185        }
1186    }
1187
1188    static NAME_MEMBER: Schema = Schema::new_member(
1189        crate::shape_id!("test", "TestStruct"),
1190        ShapeType::String,
1191        "name",
1192        0,
1193    );
1194    static MEMBERS: &[&Schema] = &[&NAME_MEMBER];
1195    static STRUCT_WITH_MEMBER: Schema = Schema::new_struct(
1196        crate::shape_id!("test", "TestStruct"),
1197        ShapeType::Structure,
1198        MEMBERS,
1199    );
1200
1201    struct NameStruct;
1202    impl SerializableStruct for NameStruct {
1203        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1204            s.write_string(&NAME_MEMBER, "Alice")
1205        }
1206    }
1207
1208    fn make_protocol() -> HttpBindingProtocol<TestCodec> {
1209        HttpBindingProtocol::new(
1210            crate::shape_id!("test", "proto"),
1211            TestCodec,
1212            "application/test",
1213        )
1214    }
1215
1216    #[test]
1217    fn serialize_sets_content_type() {
1218        // A struct with body members gets Content-Type
1219        let request = make_protocol()
1220            .serialize_request(
1221                &EmptyStruct,
1222                &STRUCT_WITH_MEMBER,
1223                "https://example.com",
1224                &ConfigBag::base(),
1225            )
1226            .unwrap();
1227        assert_eq!(
1228            request.headers().get("Content-Type").unwrap(),
1229            "application/test"
1230        );
1231    }
1232
1233    #[test]
1234    fn serialize_no_body_members_omits_content_type() {
1235        // A struct with no members gets no Content-Type per REST-JSON spec
1236        let request = make_protocol()
1237            .serialize_request(
1238                &EmptyStruct,
1239                &TEST_SCHEMA,
1240                "https://example.com",
1241                &ConfigBag::base(),
1242            )
1243            .unwrap();
1244        assert!(request.headers().get("Content-Type").is_none());
1245    }
1246
1247    #[test]
1248    fn serialize_sets_uri() {
1249        let request = make_protocol()
1250            .serialize_request(
1251                &EmptyStruct,
1252                &TEST_SCHEMA,
1253                "https://example.com/path",
1254                &ConfigBag::base(),
1255            )
1256            .unwrap();
1257        assert_eq!(request.uri(), "https://example.com/path");
1258    }
1259
1260    #[test]
1261    fn serialize_body() {
1262        let request = make_protocol()
1263            .serialize_request(
1264                &NameStruct,
1265                &STRUCT_WITH_MEMBER,
1266                "https://example.com",
1267                &ConfigBag::base(),
1268            )
1269            .unwrap();
1270        assert_eq!(request.body().bytes().unwrap(), b"{Alice}");
1271    }
1272
1273    #[test]
1274    fn deserialize_response() {
1275        let response = Response::new(
1276            200u16.try_into().unwrap(),
1277            SdkBody::from(r#"{"name":"Bob"}"#),
1278        );
1279        let mut deser = make_protocol()
1280            .deserialize_response(&response, &TEST_SCHEMA, &ConfigBag::base())
1281            .unwrap();
1282        assert_eq!(deser.read_string(&STRING).unwrap(), r#"{"name":"Bob"}"#);
1283    }
1284
1285    #[test]
1286    fn update_endpoint() {
1287        let mut request = make_protocol()
1288            .serialize_request(
1289                &EmptyStruct,
1290                &TEST_SCHEMA,
1291                "https://old.example.com",
1292                &ConfigBag::base(),
1293            )
1294            .unwrap();
1295        let endpoint = aws_smithy_types::endpoint::Endpoint::builder()
1296            .url("https://new.example.com")
1297            .build();
1298        make_protocol()
1299            .update_endpoint(&mut request, &endpoint, &ConfigBag::base())
1300            .unwrap();
1301        assert_eq!(request.uri(), "https://new.example.com/");
1302    }
1303
1304    #[test]
1305    fn protocol_id() {
1306        let protocol = HttpBindingProtocol::new(
1307            crate::shape_id!("aws.protocols", "restJson1"),
1308            TestCodec,
1309            "application/json",
1310        );
1311        assert_eq!(protocol.protocol_id().as_str(), "aws.protocols#restJson1");
1312    }
1313
1314    #[test]
1315    fn invalid_uri_returns_error() {
1316        assert!(make_protocol()
1317            .serialize_request(
1318                &EmptyStruct,
1319                &TEST_SCHEMA,
1320                "not a valid uri\n\n",
1321                &ConfigBag::base()
1322            )
1323            .is_err());
1324    }
1325
1326    // -- @httpHeader tests --
1327
1328    static HEADER_MEMBER: Schema = Schema::new_member(
1329        crate::shape_id!("test", "S"),
1330        ShapeType::String,
1331        "xToken",
1332        0,
1333    )
1334    .with_http_header("X-Token");
1335
1336    static HEADER_SCHEMA: Schema = Schema::new_struct(
1337        crate::shape_id!("test", "S"),
1338        ShapeType::Structure,
1339        &[&HEADER_MEMBER],
1340    );
1341
1342    struct HeaderStruct;
1343    impl SerializableStruct for HeaderStruct {
1344        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1345            s.write_string(&HEADER_MEMBER, "my-token-value")
1346        }
1347    }
1348
1349    #[test]
1350    fn http_header_string() {
1351        let request = make_protocol()
1352            .serialize_request(
1353                &HeaderStruct,
1354                &HEADER_SCHEMA,
1355                "https://example.com",
1356                &ConfigBag::base(),
1357            )
1358            .unwrap();
1359        assert_eq!(request.headers().get("X-Token").unwrap(), "my-token-value");
1360    }
1361
1362    static INT_HEADER_MEMBER: Schema = Schema::new_member(
1363        crate::shape_id!("test", "S"),
1364        ShapeType::Integer,
1365        "retryCount",
1366        0,
1367    )
1368    .with_http_header("X-Retry-Count");
1369
1370    static INT_HEADER_SCHEMA: Schema = Schema::new_struct(
1371        crate::shape_id!("test", "S"),
1372        ShapeType::Structure,
1373        &[&INT_HEADER_MEMBER],
1374    );
1375
1376    struct IntHeaderStruct;
1377    impl SerializableStruct for IntHeaderStruct {
1378        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1379            s.write_integer(&INT_HEADER_MEMBER, 3)
1380        }
1381    }
1382
1383    #[test]
1384    fn http_header_integer() {
1385        let request = make_protocol()
1386            .serialize_request(
1387                &IntHeaderStruct,
1388                &INT_HEADER_SCHEMA,
1389                "https://example.com",
1390                &ConfigBag::base(),
1391            )
1392            .unwrap();
1393        assert_eq!(request.headers().get("X-Retry-Count").unwrap(), "3");
1394    }
1395
1396    static BOOL_HEADER_MEMBER: Schema = Schema::new_member(
1397        crate::shape_id!("test", "S"),
1398        ShapeType::Boolean,
1399        "verbose",
1400        0,
1401    )
1402    .with_http_header("X-Verbose");
1403
1404    static BOOL_HEADER_SCHEMA: Schema = Schema::new_struct(
1405        crate::shape_id!("test", "S"),
1406        ShapeType::Structure,
1407        &[&BOOL_HEADER_MEMBER],
1408    );
1409
1410    struct BoolHeaderStruct;
1411    impl SerializableStruct for BoolHeaderStruct {
1412        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1413            s.write_boolean(&BOOL_HEADER_MEMBER, true)
1414        }
1415    }
1416
1417    #[test]
1418    fn http_header_boolean() {
1419        let request = make_protocol()
1420            .serialize_request(
1421                &BoolHeaderStruct,
1422                &BOOL_HEADER_SCHEMA,
1423                "https://example.com",
1424                &ConfigBag::base(),
1425            )
1426            .unwrap();
1427        assert_eq!(request.headers().get("X-Verbose").unwrap(), "true");
1428    }
1429
1430    // -- @httpQuery tests --
1431
1432    static QUERY_MEMBER: Schema =
1433        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "color", 0)
1434            .with_http_query("color");
1435
1436    static QUERY_SCHEMA: Schema = Schema::new_struct(
1437        crate::shape_id!("test", "S"),
1438        ShapeType::Structure,
1439        &[&QUERY_MEMBER],
1440    );
1441
1442    struct QueryStruct;
1443    impl SerializableStruct for QueryStruct {
1444        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1445            s.write_string(&QUERY_MEMBER, "blue")
1446        }
1447    }
1448
1449    #[test]
1450    fn http_query_string() {
1451        let request = make_protocol()
1452            .serialize_request(
1453                &QueryStruct,
1454                &QUERY_SCHEMA,
1455                "https://example.com/things",
1456                &ConfigBag::base(),
1457            )
1458            .unwrap();
1459        assert_eq!(request.uri(), "https://example.com/things?color=blue");
1460    }
1461
1462    static INT_QUERY_MEMBER: Schema =
1463        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Integer, "size", 0)
1464            .with_http_query("size");
1465
1466    static INT_QUERY_SCHEMA: Schema = Schema::new_struct(
1467        crate::shape_id!("test", "S"),
1468        ShapeType::Structure,
1469        &[&INT_QUERY_MEMBER],
1470    );
1471
1472    struct IntQueryStruct;
1473    impl SerializableStruct for IntQueryStruct {
1474        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1475            s.write_integer(&INT_QUERY_MEMBER, 42)
1476        }
1477    }
1478
1479    #[test]
1480    fn http_query_integer() {
1481        let request = make_protocol()
1482            .serialize_request(
1483                &IntQueryStruct,
1484                &INT_QUERY_SCHEMA,
1485                "https://example.com/things",
1486                &ConfigBag::base(),
1487            )
1488            .unwrap();
1489        assert_eq!(request.uri(), "https://example.com/things?size=42");
1490    }
1491
1492    // -- Multiple @httpQuery params --
1493
1494    static Q1: Schema =
1495        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "a", 0)
1496            .with_http_query("a");
1497    static Q2: Schema =
1498        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "b", 1)
1499            .with_http_query("b");
1500    static MULTI_QUERY_SCHEMA: Schema = Schema::new_struct(
1501        crate::shape_id!("test", "S"),
1502        ShapeType::Structure,
1503        &[&Q1, &Q2],
1504    );
1505
1506    struct MultiQueryStruct;
1507    impl SerializableStruct for MultiQueryStruct {
1508        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1509            s.write_string(&Q1, "x")?;
1510            s.write_string(&Q2, "y")
1511        }
1512    }
1513
1514    #[test]
1515    fn http_query_multiple_params() {
1516        let request = make_protocol()
1517            .serialize_request(
1518                &MultiQueryStruct,
1519                &MULTI_QUERY_SCHEMA,
1520                "https://example.com",
1521                &ConfigBag::base(),
1522            )
1523            .unwrap();
1524        assert_eq!(request.uri(), "https://example.com?a=x&b=y");
1525    }
1526
1527    // -- @httpQuery with percent-encoding --
1528
1529    #[test]
1530    fn http_query_percent_encodes_values() {
1531        struct SpaceQueryStruct;
1532        impl SerializableStruct for SpaceQueryStruct {
1533            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1534                s.write_string(&QUERY_MEMBER, "hello world")
1535            }
1536        }
1537        let request = make_protocol()
1538            .serialize_request(
1539                &SpaceQueryStruct,
1540                &QUERY_SCHEMA,
1541                "https://example.com",
1542                &ConfigBag::base(),
1543            )
1544            .unwrap();
1545        assert_eq!(request.uri(), "https://example.com?color=hello%20world");
1546    }
1547
1548    // -- @httpLabel tests --
1549
1550    static LABEL_MEMBER: Schema = Schema::new_member(
1551        crate::shape_id!("test", "S"),
1552        ShapeType::String,
1553        "bucketName",
1554        0,
1555    )
1556    .with_http_label();
1557
1558    static LABEL_SCHEMA: Schema = Schema::new_struct(
1559        crate::shape_id!("test", "S"),
1560        ShapeType::Structure,
1561        &[&LABEL_MEMBER],
1562    );
1563
1564    struct LabelStruct;
1565    impl SerializableStruct for LabelStruct {
1566        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1567            s.write_string(&LABEL_MEMBER, "my-bucket")
1568        }
1569    }
1570
1571    #[test]
1572    fn http_label_substitution() {
1573        let request = make_protocol()
1574            .serialize_request(
1575                &LabelStruct,
1576                &LABEL_SCHEMA,
1577                "https://example.com/{bucketName}/objects",
1578                &ConfigBag::base(),
1579            )
1580            .unwrap();
1581        assert_eq!(request.uri(), "https://example.com/my-bucket/objects");
1582    }
1583
1584    #[test]
1585    fn http_label_percent_encodes() {
1586        struct SpecialLabelStruct;
1587        impl SerializableStruct for SpecialLabelStruct {
1588            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1589                s.write_string(&LABEL_MEMBER, "my bucket/name")
1590            }
1591        }
1592        let request = make_protocol()
1593            .serialize_request(
1594                &SpecialLabelStruct,
1595                &LABEL_SCHEMA,
1596                "https://example.com/{bucketName}",
1597                &ConfigBag::base(),
1598            )
1599            .unwrap();
1600        assert!(request.uri().contains("my%20bucket%2Fname"));
1601    }
1602
1603    static INT_LABEL_MEMBER: Schema = Schema::new_member(
1604        crate::shape_id!("test", "S"),
1605        ShapeType::Integer,
1606        "itemId",
1607        0,
1608    )
1609    .with_http_label();
1610
1611    static INT_LABEL_SCHEMA: Schema = Schema::new_struct(
1612        crate::shape_id!("test", "S"),
1613        ShapeType::Structure,
1614        &[&INT_LABEL_MEMBER],
1615    );
1616
1617    struct IntLabelStruct;
1618    impl SerializableStruct for IntLabelStruct {
1619        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1620            s.write_integer(&INT_LABEL_MEMBER, 123)
1621        }
1622    }
1623
1624    #[test]
1625    fn http_label_integer() {
1626        let request = make_protocol()
1627            .serialize_request(
1628                &IntLabelStruct,
1629                &INT_LABEL_SCHEMA,
1630                "https://example.com/items/{itemId}",
1631                &ConfigBag::base(),
1632            )
1633            .unwrap();
1634        assert_eq!(request.uri(), "https://example.com/items/123");
1635    }
1636
1637    // -- Combined: @httpHeader + @httpQuery + @httpLabel + body --
1638
1639    static COMBINED_LABEL: Schema =
1640        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "id", 0)
1641            .with_http_label();
1642    static COMBINED_HEADER: Schema =
1643        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "token", 1)
1644            .with_http_header("X-Token");
1645    static COMBINED_QUERY: Schema = Schema::new_member(
1646        crate::shape_id!("test", "S"),
1647        ShapeType::String,
1648        "filter",
1649        2,
1650    )
1651    .with_http_query("filter");
1652    static COMBINED_BODY: Schema =
1653        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "data", 3);
1654    static COMBINED_SCHEMA: Schema = Schema::new_struct(
1655        crate::shape_id!("test", "S"),
1656        ShapeType::Structure,
1657        &[
1658            &COMBINED_LABEL,
1659            &COMBINED_HEADER,
1660            &COMBINED_QUERY,
1661            &COMBINED_BODY,
1662        ],
1663    );
1664
1665    struct CombinedStruct;
1666    impl SerializableStruct for CombinedStruct {
1667        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1668            s.write_string(&COMBINED_LABEL, "item-42")?;
1669            s.write_string(&COMBINED_HEADER, "secret")?;
1670            s.write_string(&COMBINED_QUERY, "active")?;
1671            s.write_string(&COMBINED_BODY, "payload-data")
1672        }
1673    }
1674
1675    #[test]
1676    fn combined_bindings() {
1677        let request = make_protocol()
1678            .serialize_request(
1679                &CombinedStruct,
1680                &COMBINED_SCHEMA,
1681                "https://example.com/{id}/details",
1682                &ConfigBag::base(),
1683            )
1684            .unwrap();
1685        assert_eq!(
1686            request.uri(),
1687            "https://example.com/item-42/details?filter=active"
1688        );
1689        // Header
1690        assert_eq!(request.headers().get("X-Token").unwrap(), "secret");
1691        // Body contains only the unbound member
1692        let body = request.body().bytes().unwrap();
1693        assert!(body
1694            .windows(b"payload-data".len())
1695            .any(|w| w == b"payload-data"));
1696    }
1697
1698    // -- @httpPrefixHeaders tests --
1699
1700    static PREFIX_MEMBER: Schema =
1701        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Map, "metadata", 0)
1702            .with_http_prefix_headers("X-Meta-");
1703
1704    static PREFIX_SCHEMA: Schema = Schema::new_struct(
1705        crate::shape_id!("test", "S"),
1706        ShapeType::Structure,
1707        &[&PREFIX_MEMBER],
1708    );
1709
1710    struct PrefixHeaderStruct;
1711    impl SerializableStruct for PrefixHeaderStruct {
1712        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1713            s.write_map(&PREFIX_MEMBER, &|s| {
1714                s.write_string(&STRING, "Color")?;
1715                s.write_string(&STRING, "red")?;
1716                s.write_string(&STRING, "Size")?;
1717                s.write_string(&STRING, "large")?;
1718                Ok(())
1719            })
1720        }
1721    }
1722
1723    #[test]
1724    fn http_prefix_headers() {
1725        let request = make_protocol()
1726            .serialize_request(
1727                &PrefixHeaderStruct,
1728                &PREFIX_SCHEMA,
1729                "https://example.com",
1730                &ConfigBag::base(),
1731            )
1732            .unwrap();
1733        assert_eq!(request.headers().get("X-Meta-Color").unwrap(), "red");
1734        assert_eq!(request.headers().get("X-Meta-Size").unwrap(), "large");
1735    }
1736
1737    // -- @httpQueryParams tests --
1738
1739    static QUERY_PARAMS_MEMBER: Schema =
1740        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Map, "params", 0)
1741            .with_http_query_params();
1742
1743    static QUERY_PARAMS_SCHEMA: Schema = Schema::new_struct(
1744        crate::shape_id!("test", "S"),
1745        ShapeType::Structure,
1746        &[&QUERY_PARAMS_MEMBER],
1747    );
1748
1749    struct QueryParamsStruct;
1750    impl SerializableStruct for QueryParamsStruct {
1751        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1752            s.write_map(&QUERY_PARAMS_MEMBER, &|s| {
1753                s.write_string(&STRING, "page")?;
1754                s.write_string(&STRING, "2")?;
1755                s.write_string(&STRING, "limit")?;
1756                s.write_string(&STRING, "50")?;
1757                Ok(())
1758            })
1759        }
1760    }
1761
1762    #[test]
1763    fn http_query_params() {
1764        let request = make_protocol()
1765            .serialize_request(
1766                &QueryParamsStruct,
1767                &QUERY_PARAMS_SCHEMA,
1768                "https://example.com",
1769                &ConfigBag::base(),
1770            )
1771            .unwrap();
1772        assert_eq!(request.uri(), "https://example.com?page=2&limit=50");
1773    }
1774
1775    // -- Timestamp in header defaults to http-date --
1776
1777    static TS_HEADER_MEMBER: Schema = Schema::new_member(
1778        crate::shape_id!("test", "S"),
1779        ShapeType::Timestamp,
1780        "ifModified",
1781        0,
1782    )
1783    .with_http_header("If-Modified-Since");
1784
1785    static TS_HEADER_SCHEMA: Schema = Schema::new_struct(
1786        crate::shape_id!("test", "S"),
1787        ShapeType::Structure,
1788        &[&TS_HEADER_MEMBER],
1789    );
1790
1791    struct TimestampHeaderStruct;
1792    impl SerializableStruct for TimestampHeaderStruct {
1793        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1794            s.write_timestamp(&TS_HEADER_MEMBER, &aws_smithy_types::DateTime::from_secs(0))
1795        }
1796    }
1797
1798    #[test]
1799    fn timestamp_header_uses_http_date() {
1800        let request = make_protocol()
1801            .serialize_request(
1802                &TimestampHeaderStruct,
1803                &TS_HEADER_SCHEMA,
1804                "https://example.com",
1805                &ConfigBag::base(),
1806            )
1807            .unwrap();
1808        let value = request.headers().get("If-Modified-Since").unwrap();
1809        // http-date format: "Thu, 01 Jan 1970 00:00:00 GMT"
1810        assert!(value.contains("1970"), "expected http-date, got: {value}");
1811    }
1812
1813    // -- Timestamp in query defaults to date-time --
1814
1815    static TS_QUERY_MEMBER: Schema = Schema::new_member(
1816        crate::shape_id!("test", "S"),
1817        ShapeType::Timestamp,
1818        "since",
1819        0,
1820    )
1821    .with_http_query("since");
1822
1823    static TS_QUERY_SCHEMA: Schema = Schema::new_struct(
1824        crate::shape_id!("test", "S"),
1825        ShapeType::Structure,
1826        &[&TS_QUERY_MEMBER],
1827    );
1828
1829    struct TimestampQueryStruct;
1830    impl SerializableStruct for TimestampQueryStruct {
1831        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1832            s.write_timestamp(&TS_QUERY_MEMBER, &aws_smithy_types::DateTime::from_secs(0))
1833        }
1834    }
1835
1836    #[test]
1837    fn timestamp_query_uses_date_time() {
1838        let request = make_protocol()
1839            .serialize_request(
1840                &TimestampQueryStruct,
1841                &TS_QUERY_SCHEMA,
1842                "https://example.com",
1843                &ConfigBag::base(),
1844            )
1845            .unwrap();
1846        assert_eq!(
1847            request.uri(),
1848            "https://example.com?since=1970-01-01T00%3A00%3A00Z"
1849        );
1850    }
1851
1852    // -- Unbound members go to body, bound members do not --
1853
1854    static BOUND_MEMBER: Schema = Schema::new_member(
1855        crate::shape_id!("test", "S"),
1856        ShapeType::String,
1857        "headerVal",
1858        0,
1859    )
1860    .with_http_header("X-Val");
1861    static UNBOUND_MEMBER: Schema = Schema::new_member(
1862        crate::shape_id!("test", "S"),
1863        ShapeType::String,
1864        "bodyVal",
1865        1,
1866    );
1867    static MIXED_SCHEMA: Schema = Schema::new_struct(
1868        crate::shape_id!("test", "S"),
1869        ShapeType::Structure,
1870        &[&BOUND_MEMBER, &UNBOUND_MEMBER],
1871    );
1872
1873    struct MixedStruct;
1874    impl SerializableStruct for MixedStruct {
1875        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1876            s.write_string(&BOUND_MEMBER, "in-header")?;
1877            s.write_string(&UNBOUND_MEMBER, "in-body")
1878        }
1879    }
1880
1881    #[test]
1882    fn bound_members_not_in_body() {
1883        let request = make_protocol()
1884            .serialize_request(
1885                &MixedStruct,
1886                &MIXED_SCHEMA,
1887                "https://example.com",
1888                &ConfigBag::base(),
1889            )
1890            .unwrap();
1891        let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
1892        assert!(
1893            body.contains("in-body"),
1894            "body should contain unbound member"
1895        );
1896        assert!(
1897            !body.contains("in-header"),
1898            "body should NOT contain header-bound member"
1899        );
1900        assert_eq!(request.headers().get("X-Val").unwrap(), "in-header");
1901    }
1902}