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::{apply_http_endpoint, ClientProtocolInner};
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/// Whether a `ListElementCollector` is gathering values for a header or query param.
503/// Affects default timestamp format: `http-date` for headers, `date-time` for query.
504#[derive(Copy, Clone)]
505enum HttpListTarget {
506    Header,
507    Query,
508}
509
510/// Collects list element values as strings for @httpHeader and @httpQuery on lists.
511struct ListElementCollector {
512    values: Vec<String>,
513    /// Whether each value should be quoted if it contains commas (strings yes, timestamps no)
514    quotable: Vec<bool>,
515    target: HttpListTarget,
516}
517
518impl ListElementCollector {
519    fn for_header() -> Self {
520        Self::new(HttpListTarget::Header)
521    }
522
523    fn for_query() -> Self {
524        Self::new(HttpListTarget::Query)
525    }
526
527    fn new(target: HttpListTarget) -> Self {
528        Self {
529            values: Vec::new(),
530            quotable: Vec::new(),
531            target,
532        }
533    }
534
535    fn push(&mut self, value: String) {
536        self.quotable.push(true);
537        self.values.push(value);
538    }
539
540    fn push_unquotable(&mut self, value: String) {
541        self.quotable.push(false);
542        self.values.push(value);
543    }
544}
545
546impl ShapeSerializer for ListElementCollector {
547    fn write_string(&mut self, _schema: &Schema, value: &str) -> Result<(), SerdeError> {
548        self.push(value.to_string());
549        Ok(())
550    }
551    fn write_boolean(&mut self, _: &Schema, value: bool) -> Result<(), SerdeError> {
552        self.push(value.to_string());
553        Ok(())
554    }
555    fn write_byte(&mut self, _: &Schema, value: i8) -> Result<(), SerdeError> {
556        self.push(value.to_string());
557        Ok(())
558    }
559    fn write_short(&mut self, _: &Schema, value: i16) -> Result<(), SerdeError> {
560        self.push(value.to_string());
561        Ok(())
562    }
563    fn write_integer(&mut self, _: &Schema, value: i32) -> Result<(), SerdeError> {
564        self.push(value.to_string());
565        Ok(())
566    }
567    fn write_long(&mut self, _: &Schema, value: i64) -> Result<(), SerdeError> {
568        self.push(value.to_string());
569        Ok(())
570    }
571    fn write_float(&mut self, _: &Schema, value: f32) -> Result<(), SerdeError> {
572        self.push(format_float_f32(value));
573        Ok(())
574    }
575    fn write_double(&mut self, _: &Schema, value: f64) -> Result<(), SerdeError> {
576        self.push(format_float_f64(value));
577        Ok(())
578    }
579    fn write_timestamp(
580        &mut self,
581        schema: &Schema,
582        value: &aws_smithy_types::DateTime,
583    ) -> Result<(), SerdeError> {
584        let format = match schema.timestamp_format() {
585            Some(ts) => match ts.format() {
586                crate::traits::TimestampFormat::EpochSeconds => {
587                    aws_smithy_types::date_time::Format::EpochSeconds
588                }
589                crate::traits::TimestampFormat::HttpDate => {
590                    aws_smithy_types::date_time::Format::HttpDate
591                }
592                crate::traits::TimestampFormat::DateTime => {
593                    aws_smithy_types::date_time::Format::DateTime
594                }
595            },
596            // Default: headers use http-date, query params use date-time
597            None => match self.target {
598                HttpListTarget::Header => aws_smithy_types::date_time::Format::HttpDate,
599                HttpListTarget::Query => aws_smithy_types::date_time::Format::DateTime,
600            },
601        };
602        self.push_unquotable(
603            value
604                .fmt(format)
605                .map_err(|e| SerdeError::custom(format!("failed to format timestamp: {e}")))?,
606        );
607        Ok(())
608    }
609    fn write_blob(
610        &mut self,
611        _schema: &Schema,
612        value: &aws_smithy_types::Blob,
613    ) -> Result<(), SerdeError> {
614        self.push(aws_smithy_types::base64::encode(value.as_ref()));
615        Ok(())
616    }
617    // Remaining methods are no-ops for list element collection
618    fn write_struct(&mut self, _: &Schema, _: &dyn SerializableStruct) -> Result<(), SerdeError> {
619        Ok(())
620    }
621    fn write_list(
622        &mut self,
623        _: &Schema,
624        _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
625    ) -> Result<(), SerdeError> {
626        Ok(())
627    }
628    fn write_map(
629        &mut self,
630        _: &Schema,
631        _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
632    ) -> Result<(), SerdeError> {
633        Ok(())
634    }
635    fn write_big_integer(
636        &mut self,
637        _: &Schema,
638        _: &aws_smithy_types::BigInteger,
639    ) -> Result<(), SerdeError> {
640        Ok(())
641    }
642    fn write_big_decimal(
643        &mut self,
644        _: &Schema,
645        _: &aws_smithy_types::BigDecimal,
646    ) -> Result<(), SerdeError> {
647        Ok(())
648    }
649    fn write_document(
650        &mut self,
651        _: &Schema,
652        _: &aws_smithy_types::Document,
653    ) -> Result<(), SerdeError> {
654        Ok(())
655    }
656    fn write_null(&mut self, _: &Schema) -> Result<(), SerdeError> {
657        Ok(())
658    }
659}
660
661/// Format a float for HTTP headers/query/labels.
662/// Rust's Display writes "inf"/"-inf" but HTTP requires "Infinity"/"-Infinity".
663fn format_float_f32(value: f32) -> String {
664    if value.is_infinite() {
665        if value.is_sign_positive() {
666            "Infinity".to_string()
667        } else {
668            "-Infinity".to_string()
669        }
670    } else if value.is_nan() {
671        "NaN".to_string()
672    } else {
673        value.to_string()
674    }
675}
676
677fn format_float_f64(value: f64) -> String {
678    if value.is_infinite() {
679        if value.is_sign_positive() {
680            "Infinity".to_string()
681        } else {
682            "-Infinity".to_string()
683        }
684    } else if value.is_nan() {
685        "NaN".to_string()
686    } else {
687        value.to_string()
688    }
689}
690
691/// Collects map key-value pairs written via ShapeSerializer for
692/// @httpPrefixHeaders and @httpQueryParams.
693struct MapEntryCollector {
694    prefix: String,
695    entries: Vec<(String, String)>,
696    pending_key: Option<String>,
697}
698
699impl MapEntryCollector {
700    fn new(prefix: String) -> Self {
701        Self {
702            prefix,
703            entries: Vec::new(),
704            pending_key: None,
705        }
706    }
707}
708
709impl ShapeSerializer for MapEntryCollector {
710    fn write_string(&mut self, _schema: &Schema, value: &str) -> Result<(), SerdeError> {
711        if let Some(key) = self.pending_key.take() {
712            self.entries
713                .push((format!("{}{}", self.prefix, key), value.to_string()));
714        } else {
715            self.pending_key = Some(value.to_string());
716        }
717        Ok(())
718    }
719
720    // All other methods are no-ops — maps in HTTP bindings only have string keys/values.
721    // Exception: write_list handles Map<String, List<String>> for @httpQueryParams.
722    fn write_struct(&mut self, _: &Schema, _: &dyn SerializableStruct) -> Result<(), SerdeError> {
723        Ok(())
724    }
725    fn write_list(
726        &mut self,
727        _: &Schema,
728        write_elements: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
729    ) -> Result<(), SerdeError> {
730        // Map<String, List<String>>: each list element becomes a separate entry
731        // with the same key (for @httpQueryParams).
732        if let Some(key) = self.pending_key.take() {
733            let mut collector = ListElementCollector::for_query(); // query params context
734            write_elements(&mut collector)?;
735            for val in collector.values {
736                self.entries.push((format!("{}{}", self.prefix, key), val));
737            }
738        }
739        Ok(())
740    }
741    fn write_map(
742        &mut self,
743        _: &Schema,
744        _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
745    ) -> Result<(), SerdeError> {
746        Ok(())
747    }
748    fn write_boolean(&mut self, _: &Schema, _: bool) -> Result<(), SerdeError> {
749        Ok(())
750    }
751    fn write_byte(&mut self, _: &Schema, _: i8) -> Result<(), SerdeError> {
752        Ok(())
753    }
754    fn write_short(&mut self, _: &Schema, _: i16) -> Result<(), SerdeError> {
755        Ok(())
756    }
757    fn write_integer(&mut self, _: &Schema, _: i32) -> Result<(), SerdeError> {
758        Ok(())
759    }
760    fn write_long(&mut self, _: &Schema, _: i64) -> Result<(), SerdeError> {
761        Ok(())
762    }
763    fn write_float(&mut self, _: &Schema, _: f32) -> Result<(), SerdeError> {
764        Ok(())
765    }
766    fn write_double(&mut self, _: &Schema, _: f64) -> Result<(), SerdeError> {
767        Ok(())
768    }
769    fn write_big_integer(
770        &mut self,
771        _: &Schema,
772        _: &aws_smithy_types::BigInteger,
773    ) -> Result<(), SerdeError> {
774        Ok(())
775    }
776    fn write_big_decimal(
777        &mut self,
778        _: &Schema,
779        _: &aws_smithy_types::BigDecimal,
780    ) -> Result<(), SerdeError> {
781        Ok(())
782    }
783    fn write_blob(&mut self, _: &Schema, _: &aws_smithy_types::Blob) -> Result<(), SerdeError> {
784        Ok(())
785    }
786    fn write_timestamp(
787        &mut self,
788        _: &Schema,
789        _: &aws_smithy_types::DateTime,
790    ) -> Result<(), SerdeError> {
791        Ok(())
792    }
793    fn write_document(
794        &mut self,
795        _: &Schema,
796        _: &aws_smithy_types::Document,
797    ) -> Result<(), SerdeError> {
798        Ok(())
799    }
800    fn write_null(&mut self, _: &Schema) -> Result<(), SerdeError> {
801        Ok(())
802    }
803}
804
805impl<C> ClientProtocolInner for HttpBindingProtocol<C>
806where
807    C: Codec + Send + Sync + std::fmt::Debug + 'static,
808    for<'a> C::Deserializer<'a>: ShapeDeserializer,
809{
810    type Request = Request;
811    type Response = Response;
812
813    fn protocol_id(&self) -> &ShapeId {
814        &self.protocol_id
815    }
816
817    fn serialize_request(
818        &self,
819        input: &dyn SerializableStruct,
820        input_schema: &Schema,
821        endpoint: &str,
822        _cfg: &ConfigBag,
823    ) -> Result<Request, SerdeError> {
824        let mut binder =
825            HttpBindingSerializer::new(self.codec.create_serializer(), Some(input_schema));
826
827        // Check if there's an @httpPayload member targeting a structure/union.
828        // In that case, the payload member's own write_struct provides the body
829        // framing, so we must not add top-level struct framing.
830        let has_struct_payload = input_schema.members().iter().any(|m| {
831            m.http_payload().is_some()
832                && matches!(
833                    m.shape_type(),
834                    crate::ShapeType::Structure | crate::ShapeType::Union
835                )
836        });
837        if has_struct_payload {
838            binder.is_top_level = false;
839            input.serialize_members(&mut binder)?;
840        } else {
841            binder.write_struct(input_schema, input)?;
842        }
843        let raw_payload = binder.raw_payload;
844        let mut body = if raw_payload.is_some() {
845            // @httpPayload blob/string — don't use the codec output
846            Vec::new()
847        } else {
848            binder.body.finish()
849        };
850
851        // Per the REST-JSON content-type handling spec:
852        // - If @httpPayload targets a blob/string: send raw bytes, no Content-Type when empty
853        // - If body members exist (even if all optional and unset): send `{}` with Content-Type
854        // - If no body members at all (everything is in headers/query/labels): empty body, no Content-Type
855        let has_blob_or_string_payload = raw_payload.is_some();
856        let has_body_members = has_struct_payload
857            || input_schema.members().iter().any(|m| {
858                m.http_header().is_none()
859                    && m.http_query().is_none()
860                    && m.http_label().is_none()
861                    && m.http_prefix_headers().is_none()
862                    && m.http_query_params().is_none()
863                    && m.http_payload().is_none()
864            });
865
866        let set_content_type = if has_blob_or_string_payload {
867            // Blob/string payload: Content-Type comes from the @httpHeader("Content-Type")
868            // member if present, or defaults to application/octet-stream for blobs.
869            // Don't set the protocol's codec content type (e.g., application/json).
870            false
871        } else if has_body_members {
872            // Operation has body members — body includes framing (e.g., `{}`).
873            // Per the REST-JSON spec, even if all members are optional and unset, send `{}`.
874            true
875        } else {
876            // No body members at all — empty body, no Content-Type.
877            body = Vec::new();
878            false
879        };
880
881        // Build URI: use @http trait if available (with label substitution from binder),
882        // otherwise fall back to endpoint with manual label substitution.
883        let mut uri = match input_schema.http() {
884            Some(h) => {
885                let mut path = h.uri().to_string();
886                for (name, value) in &binder.labels {
887                    // Try greedy label first ({name+}), then regular ({name})
888                    let greedy = format!("{{{name}+}}");
889                    if path.contains(&greedy) {
890                        // Greedy labels: encode each path segment separately, preserve /
891                        let encoded = value
892                            .split('/')
893                            .map(percent_encode)
894                            .collect::<Vec<_>>()
895                            .join("/");
896                        path = path.replace(&greedy, &encoded);
897                    } else {
898                        let placeholder = format!("{{{name}}}");
899                        path = path.replace(&placeholder, &percent_encode(value));
900                    }
901                }
902                if endpoint.is_empty() {
903                    path
904                } else {
905                    format!("{}{}", endpoint, path)
906                }
907            }
908            None => {
909                let mut u = if endpoint.is_empty() {
910                    "/".to_string()
911                } else {
912                    endpoint.to_string()
913                };
914                for (name, value) in &binder.labels {
915                    let greedy = format!("{{{name}+}}");
916                    if u.contains(&greedy) {
917                        let encoded = value
918                            .split('/')
919                            .map(percent_encode)
920                            .collect::<Vec<_>>()
921                            .join("/");
922                        u = u.replace(&greedy, &encoded);
923                    } else {
924                        let placeholder = format!("{{{name}}}");
925                        u = u.replace(&placeholder, &percent_encode(value));
926                    }
927                }
928                u
929            }
930        };
931        if !binder.query_params.is_empty() {
932            uri.push(if uri.contains('?') { '&' } else { '?' });
933            let pairs: Vec<String> = binder
934                .query_params
935                .iter()
936                .map(|(k, v)| format!("{}={}", percent_encode(k), percent_encode(v)))
937                .collect();
938            uri.push_str(&pairs.join("&"));
939        }
940
941        let mut request = if let Some(payload) = raw_payload {
942            Request::new(SdkBody::from(payload))
943        } else {
944            Request::new(SdkBody::from(body))
945        };
946        // Set HTTP method from @http trait
947        if let Some(http) = input_schema.http() {
948            request
949                .set_method(http.method())
950                .map_err(|e| SerdeError::custom(format!("invalid HTTP method: {e}")))?;
951        }
952        request
953            .set_uri(uri.as_str())
954            .map_err(|e| SerdeError::custom(format!("invalid endpoint URI: {e}")))?;
955        if set_content_type {
956            request
957                .headers_mut()
958                .insert("Content-Type", self.content_type);
959        }
960        if let Some(len) = request.body().content_length() {
961            if len > 0 || set_content_type {
962                request
963                    .headers_mut()
964                    .insert("Content-Length", len.to_string());
965            }
966        }
967        for (name, value) in &binder.headers {
968            request.headers_mut().insert(name.clone(), value.clone());
969        }
970        Ok(request)
971    }
972
973    fn deserialize_response<'a>(
974        &self,
975        response: &'a Response,
976        _output_schema: &Schema,
977        _cfg: &ConfigBag,
978    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
979        let body = response
980            .body()
981            .bytes()
982            .ok_or_else(|| SerdeError::custom("response body is not available as bytes"))?;
983        Ok(Box::new(self.codec.create_deserializer(body)))
984    }
985
986    fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec> {
987        Some(&self.codec)
988    }
989
990    fn update_endpoint(
991        &self,
992        request: &mut Request,
993        endpoint: &aws_smithy_types::endpoint::Endpoint,
994        cfg: &ConfigBag,
995    ) -> Result<(), SerdeError> {
996        apply_http_endpoint(request, endpoint, cfg)
997    }
998}
999
1000#[cfg(test)]
1001mod tests {
1002    use super::*;
1003    use crate::serde::SerializableStruct;
1004    use crate::{prelude::*, ShapeType};
1005
1006    struct TestSerializer {
1007        output: Vec<u8>,
1008    }
1009
1010    impl FinishSerializer for TestSerializer {
1011        fn finish(self) -> Vec<u8> {
1012            self.output
1013        }
1014    }
1015
1016    impl ShapeSerializer for TestSerializer {
1017        fn write_struct(
1018            &mut self,
1019            _: &Schema,
1020            value: &dyn SerializableStruct,
1021        ) -> Result<(), SerdeError> {
1022            self.output.push(b'{');
1023            value.serialize_members(self)?;
1024            self.output.push(b'}');
1025            Ok(())
1026        }
1027        fn write_list(
1028            &mut self,
1029            _: &Schema,
1030            _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
1031        ) -> Result<(), SerdeError> {
1032            Ok(())
1033        }
1034        fn write_map(
1035            &mut self,
1036            _: &Schema,
1037            _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
1038        ) -> Result<(), SerdeError> {
1039            Ok(())
1040        }
1041        fn write_boolean(&mut self, _: &Schema, _: bool) -> Result<(), SerdeError> {
1042            Ok(())
1043        }
1044        fn write_byte(&mut self, _: &Schema, _: i8) -> Result<(), SerdeError> {
1045            Ok(())
1046        }
1047        fn write_short(&mut self, _: &Schema, _: i16) -> Result<(), SerdeError> {
1048            Ok(())
1049        }
1050        fn write_integer(&mut self, _: &Schema, _: i32) -> Result<(), SerdeError> {
1051            Ok(())
1052        }
1053        fn write_long(&mut self, _: &Schema, _: i64) -> Result<(), SerdeError> {
1054            Ok(())
1055        }
1056        fn write_float(&mut self, _: &Schema, _: f32) -> Result<(), SerdeError> {
1057            Ok(())
1058        }
1059        fn write_double(&mut self, _: &Schema, _: f64) -> Result<(), SerdeError> {
1060            Ok(())
1061        }
1062        fn write_big_integer(
1063            &mut self,
1064            _: &Schema,
1065            _: &aws_smithy_types::BigInteger,
1066        ) -> Result<(), SerdeError> {
1067            Ok(())
1068        }
1069        fn write_big_decimal(
1070            &mut self,
1071            _: &Schema,
1072            _: &aws_smithy_types::BigDecimal,
1073        ) -> Result<(), SerdeError> {
1074            Ok(())
1075        }
1076        fn write_string(&mut self, _: &Schema, v: &str) -> Result<(), SerdeError> {
1077            self.output.extend_from_slice(v.as_bytes());
1078            Ok(())
1079        }
1080        fn write_blob(&mut self, _: &Schema, _: &aws_smithy_types::Blob) -> Result<(), SerdeError> {
1081            Ok(())
1082        }
1083        fn write_timestamp(
1084            &mut self,
1085            _: &Schema,
1086            _: &aws_smithy_types::DateTime,
1087        ) -> Result<(), SerdeError> {
1088            Ok(())
1089        }
1090        fn write_document(
1091            &mut self,
1092            _: &Schema,
1093            _: &aws_smithy_types::Document,
1094        ) -> Result<(), SerdeError> {
1095            Ok(())
1096        }
1097        fn write_null(&mut self, _: &Schema) -> Result<(), SerdeError> {
1098            Ok(())
1099        }
1100    }
1101
1102    struct TestDeserializer<'a> {
1103        input: &'a [u8],
1104    }
1105
1106    impl ShapeDeserializer for TestDeserializer<'_> {
1107        fn read_struct(
1108            &mut self,
1109            _: &Schema,
1110            _: &mut dyn FnMut(&Schema, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
1111        ) -> Result<(), SerdeError> {
1112            Ok(())
1113        }
1114        fn read_list(
1115            &mut self,
1116            _: &Schema,
1117            _: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
1118        ) -> Result<(), SerdeError> {
1119            Ok(())
1120        }
1121        fn read_map(
1122            &mut self,
1123            _: &Schema,
1124            _: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
1125        ) -> Result<(), SerdeError> {
1126            Ok(())
1127        }
1128        fn read_boolean(&mut self, _: &Schema) -> Result<bool, SerdeError> {
1129            Ok(false)
1130        }
1131        fn read_byte(&mut self, _: &Schema) -> Result<i8, SerdeError> {
1132            Ok(0)
1133        }
1134        fn read_short(&mut self, _: &Schema) -> Result<i16, SerdeError> {
1135            Ok(0)
1136        }
1137        fn read_integer(&mut self, _: &Schema) -> Result<i32, SerdeError> {
1138            Ok(0)
1139        }
1140        fn read_long(&mut self, _: &Schema) -> Result<i64, SerdeError> {
1141            Ok(0)
1142        }
1143        fn read_float(&mut self, _: &Schema) -> Result<f32, SerdeError> {
1144            Ok(0.0)
1145        }
1146        fn read_double(&mut self, _: &Schema) -> Result<f64, SerdeError> {
1147            Ok(0.0)
1148        }
1149        fn read_big_integer(
1150            &mut self,
1151            _: &Schema,
1152        ) -> Result<aws_smithy_types::BigInteger, SerdeError> {
1153            use std::str::FromStr;
1154            Ok(aws_smithy_types::BigInteger::from_str("0").unwrap())
1155        }
1156        fn read_big_decimal(
1157            &mut self,
1158            _: &Schema,
1159        ) -> Result<aws_smithy_types::BigDecimal, SerdeError> {
1160            use std::str::FromStr;
1161            Ok(aws_smithy_types::BigDecimal::from_str("0").unwrap())
1162        }
1163        fn read_string(&mut self, _: &Schema) -> Result<String, SerdeError> {
1164            Ok(String::from_utf8_lossy(self.input).into_owned())
1165        }
1166        fn read_blob(&mut self, _: &Schema) -> Result<aws_smithy_types::Blob, SerdeError> {
1167            Ok(aws_smithy_types::Blob::new(vec![]))
1168        }
1169        fn read_timestamp(&mut self, _: &Schema) -> Result<aws_smithy_types::DateTime, SerdeError> {
1170            Ok(aws_smithy_types::DateTime::from_secs(0))
1171        }
1172        fn read_document(&mut self, _: &Schema) -> Result<aws_smithy_types::Document, SerdeError> {
1173            Ok(aws_smithy_types::Document::Null)
1174        }
1175        fn is_null(&self) -> bool {
1176            false
1177        }
1178        fn container_size(&self) -> Option<usize> {
1179            None
1180        }
1181    }
1182
1183    #[derive(Debug)]
1184    struct TestCodec;
1185
1186    impl Codec for TestCodec {
1187        type Serializer = TestSerializer;
1188        type Deserializer<'a> = TestDeserializer<'a>;
1189        fn create_serializer(&self) -> Self::Serializer {
1190            TestSerializer { output: Vec::new() }
1191        }
1192        fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
1193            TestDeserializer { input }
1194        }
1195    }
1196
1197    static TEST_SCHEMA: Schema =
1198        Schema::new(crate::shape_id!("test", "TestStruct"), ShapeType::Structure);
1199
1200    struct EmptyStruct;
1201    impl SerializableStruct for EmptyStruct {
1202        fn serialize_members(&self, _: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1203            Ok(())
1204        }
1205    }
1206
1207    static NAME_MEMBER: Schema = Schema::new_member(
1208        crate::shape_id!("test", "TestStruct"),
1209        ShapeType::String,
1210        "name",
1211        0,
1212    );
1213    static MEMBERS: &[&Schema] = &[&NAME_MEMBER];
1214    static STRUCT_WITH_MEMBER: Schema = Schema::new_struct(
1215        crate::shape_id!("test", "TestStruct"),
1216        ShapeType::Structure,
1217        MEMBERS,
1218    );
1219
1220    struct NameStruct;
1221    impl SerializableStruct for NameStruct {
1222        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1223            s.write_string(&NAME_MEMBER, "Alice")
1224        }
1225    }
1226
1227    fn make_protocol() -> HttpBindingProtocol<TestCodec> {
1228        HttpBindingProtocol::new(
1229            crate::shape_id!("test", "proto"),
1230            TestCodec,
1231            "application/test",
1232        )
1233    }
1234
1235    #[test]
1236    fn serialize_sets_content_type() {
1237        // A struct with body members gets Content-Type
1238        let request = make_protocol()
1239            .serialize_request(
1240                &EmptyStruct,
1241                &STRUCT_WITH_MEMBER,
1242                "https://example.com",
1243                &ConfigBag::base(),
1244            )
1245            .unwrap();
1246        assert_eq!(
1247            request.headers().get("Content-Type").unwrap(),
1248            "application/test"
1249        );
1250    }
1251
1252    #[test]
1253    fn serialize_no_body_members_omits_content_type() {
1254        // A struct with no members gets no Content-Type per REST-JSON spec
1255        let request = make_protocol()
1256            .serialize_request(
1257                &EmptyStruct,
1258                &TEST_SCHEMA,
1259                "https://example.com",
1260                &ConfigBag::base(),
1261            )
1262            .unwrap();
1263        assert!(request.headers().get("Content-Type").is_none());
1264    }
1265
1266    #[test]
1267    fn serialize_sets_uri() {
1268        let request = make_protocol()
1269            .serialize_request(
1270                &EmptyStruct,
1271                &TEST_SCHEMA,
1272                "https://example.com/path",
1273                &ConfigBag::base(),
1274            )
1275            .unwrap();
1276        assert_eq!(request.uri(), "https://example.com/path");
1277    }
1278
1279    #[test]
1280    fn serialize_body() {
1281        let request = make_protocol()
1282            .serialize_request(
1283                &NameStruct,
1284                &STRUCT_WITH_MEMBER,
1285                "https://example.com",
1286                &ConfigBag::base(),
1287            )
1288            .unwrap();
1289        assert_eq!(request.body().bytes().unwrap(), b"{Alice}");
1290    }
1291
1292    #[test]
1293    fn deserialize_response() {
1294        let response = Response::new(
1295            200u16.try_into().unwrap(),
1296            SdkBody::from(r#"{"name":"Bob"}"#),
1297        );
1298        let mut deser = make_protocol()
1299            .deserialize_response(&response, &TEST_SCHEMA, &ConfigBag::base())
1300            .unwrap();
1301        assert_eq!(deser.read_string(&STRING).unwrap(), r#"{"name":"Bob"}"#);
1302    }
1303
1304    #[test]
1305    fn update_endpoint() {
1306        let mut request = make_protocol()
1307            .serialize_request(
1308                &EmptyStruct,
1309                &TEST_SCHEMA,
1310                "https://old.example.com",
1311                &ConfigBag::base(),
1312            )
1313            .unwrap();
1314        let endpoint = aws_smithy_types::endpoint::Endpoint::builder()
1315            .url("https://new.example.com")
1316            .build();
1317        make_protocol()
1318            .update_endpoint(&mut request, &endpoint, &ConfigBag::base())
1319            .unwrap();
1320        assert_eq!(request.uri(), "https://new.example.com/");
1321    }
1322
1323    #[test]
1324    fn protocol_id() {
1325        let protocol = HttpBindingProtocol::new(
1326            crate::shape_id!("aws.protocols", "restJson1"),
1327            TestCodec,
1328            "application/json",
1329        );
1330        assert_eq!(protocol.protocol_id().as_str(), "aws.protocols#restJson1");
1331    }
1332
1333    #[test]
1334    fn invalid_uri_returns_error() {
1335        assert!(make_protocol()
1336            .serialize_request(
1337                &EmptyStruct,
1338                &TEST_SCHEMA,
1339                "not a valid uri\n\n",
1340                &ConfigBag::base()
1341            )
1342            .is_err());
1343    }
1344
1345    // -- @httpHeader tests --
1346
1347    static HEADER_MEMBER: Schema = Schema::new_member(
1348        crate::shape_id!("test", "S"),
1349        ShapeType::String,
1350        "xToken",
1351        0,
1352    )
1353    .with_http_header("X-Token");
1354
1355    static HEADER_SCHEMA: Schema = Schema::new_struct(
1356        crate::shape_id!("test", "S"),
1357        ShapeType::Structure,
1358        &[&HEADER_MEMBER],
1359    );
1360
1361    struct HeaderStruct;
1362    impl SerializableStruct for HeaderStruct {
1363        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1364            s.write_string(&HEADER_MEMBER, "my-token-value")
1365        }
1366    }
1367
1368    #[test]
1369    fn http_header_string() {
1370        let request = make_protocol()
1371            .serialize_request(
1372                &HeaderStruct,
1373                &HEADER_SCHEMA,
1374                "https://example.com",
1375                &ConfigBag::base(),
1376            )
1377            .unwrap();
1378        assert_eq!(request.headers().get("X-Token").unwrap(), "my-token-value");
1379    }
1380
1381    static INT_HEADER_MEMBER: Schema = Schema::new_member(
1382        crate::shape_id!("test", "S"),
1383        ShapeType::Integer,
1384        "retryCount",
1385        0,
1386    )
1387    .with_http_header("X-Retry-Count");
1388
1389    static INT_HEADER_SCHEMA: Schema = Schema::new_struct(
1390        crate::shape_id!("test", "S"),
1391        ShapeType::Structure,
1392        &[&INT_HEADER_MEMBER],
1393    );
1394
1395    struct IntHeaderStruct;
1396    impl SerializableStruct for IntHeaderStruct {
1397        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1398            s.write_integer(&INT_HEADER_MEMBER, 3)
1399        }
1400    }
1401
1402    #[test]
1403    fn http_header_integer() {
1404        let request = make_protocol()
1405            .serialize_request(
1406                &IntHeaderStruct,
1407                &INT_HEADER_SCHEMA,
1408                "https://example.com",
1409                &ConfigBag::base(),
1410            )
1411            .unwrap();
1412        assert_eq!(request.headers().get("X-Retry-Count").unwrap(), "3");
1413    }
1414
1415    static BOOL_HEADER_MEMBER: Schema = Schema::new_member(
1416        crate::shape_id!("test", "S"),
1417        ShapeType::Boolean,
1418        "verbose",
1419        0,
1420    )
1421    .with_http_header("X-Verbose");
1422
1423    static BOOL_HEADER_SCHEMA: Schema = Schema::new_struct(
1424        crate::shape_id!("test", "S"),
1425        ShapeType::Structure,
1426        &[&BOOL_HEADER_MEMBER],
1427    );
1428
1429    struct BoolHeaderStruct;
1430    impl SerializableStruct for BoolHeaderStruct {
1431        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1432            s.write_boolean(&BOOL_HEADER_MEMBER, true)
1433        }
1434    }
1435
1436    #[test]
1437    fn http_header_boolean() {
1438        let request = make_protocol()
1439            .serialize_request(
1440                &BoolHeaderStruct,
1441                &BOOL_HEADER_SCHEMA,
1442                "https://example.com",
1443                &ConfigBag::base(),
1444            )
1445            .unwrap();
1446        assert_eq!(request.headers().get("X-Verbose").unwrap(), "true");
1447    }
1448
1449    // -- @httpQuery tests --
1450
1451    static QUERY_MEMBER: Schema =
1452        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "color", 0)
1453            .with_http_query("color");
1454
1455    static QUERY_SCHEMA: Schema = Schema::new_struct(
1456        crate::shape_id!("test", "S"),
1457        ShapeType::Structure,
1458        &[&QUERY_MEMBER],
1459    );
1460
1461    struct QueryStruct;
1462    impl SerializableStruct for QueryStruct {
1463        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1464            s.write_string(&QUERY_MEMBER, "blue")
1465        }
1466    }
1467
1468    #[test]
1469    fn http_query_string() {
1470        let request = make_protocol()
1471            .serialize_request(
1472                &QueryStruct,
1473                &QUERY_SCHEMA,
1474                "https://example.com/things",
1475                &ConfigBag::base(),
1476            )
1477            .unwrap();
1478        assert_eq!(request.uri(), "https://example.com/things?color=blue");
1479    }
1480
1481    static INT_QUERY_MEMBER: Schema =
1482        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Integer, "size", 0)
1483            .with_http_query("size");
1484
1485    static INT_QUERY_SCHEMA: Schema = Schema::new_struct(
1486        crate::shape_id!("test", "S"),
1487        ShapeType::Structure,
1488        &[&INT_QUERY_MEMBER],
1489    );
1490
1491    struct IntQueryStruct;
1492    impl SerializableStruct for IntQueryStruct {
1493        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1494            s.write_integer(&INT_QUERY_MEMBER, 42)
1495        }
1496    }
1497
1498    #[test]
1499    fn http_query_integer() {
1500        let request = make_protocol()
1501            .serialize_request(
1502                &IntQueryStruct,
1503                &INT_QUERY_SCHEMA,
1504                "https://example.com/things",
1505                &ConfigBag::base(),
1506            )
1507            .unwrap();
1508        assert_eq!(request.uri(), "https://example.com/things?size=42");
1509    }
1510
1511    // -- Multiple @httpQuery params --
1512
1513    static Q1: Schema =
1514        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "a", 0)
1515            .with_http_query("a");
1516    static Q2: Schema =
1517        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "b", 1)
1518            .with_http_query("b");
1519    static MULTI_QUERY_SCHEMA: Schema = Schema::new_struct(
1520        crate::shape_id!("test", "S"),
1521        ShapeType::Structure,
1522        &[&Q1, &Q2],
1523    );
1524
1525    struct MultiQueryStruct;
1526    impl SerializableStruct for MultiQueryStruct {
1527        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1528            s.write_string(&Q1, "x")?;
1529            s.write_string(&Q2, "y")
1530        }
1531    }
1532
1533    #[test]
1534    fn http_query_multiple_params() {
1535        let request = make_protocol()
1536            .serialize_request(
1537                &MultiQueryStruct,
1538                &MULTI_QUERY_SCHEMA,
1539                "https://example.com",
1540                &ConfigBag::base(),
1541            )
1542            .unwrap();
1543        assert_eq!(request.uri(), "https://example.com?a=x&b=y");
1544    }
1545
1546    // -- @httpQuery with percent-encoding --
1547
1548    #[test]
1549    fn http_query_percent_encodes_values() {
1550        struct SpaceQueryStruct;
1551        impl SerializableStruct for SpaceQueryStruct {
1552            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1553                s.write_string(&QUERY_MEMBER, "hello world")
1554            }
1555        }
1556        let request = make_protocol()
1557            .serialize_request(
1558                &SpaceQueryStruct,
1559                &QUERY_SCHEMA,
1560                "https://example.com",
1561                &ConfigBag::base(),
1562            )
1563            .unwrap();
1564        assert_eq!(request.uri(), "https://example.com?color=hello%20world");
1565    }
1566
1567    // -- @httpLabel tests --
1568
1569    static LABEL_MEMBER: Schema = Schema::new_member(
1570        crate::shape_id!("test", "S"),
1571        ShapeType::String,
1572        "bucketName",
1573        0,
1574    )
1575    .with_http_label();
1576
1577    static LABEL_SCHEMA: Schema = Schema::new_struct(
1578        crate::shape_id!("test", "S"),
1579        ShapeType::Structure,
1580        &[&LABEL_MEMBER],
1581    );
1582
1583    struct LabelStruct;
1584    impl SerializableStruct for LabelStruct {
1585        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1586            s.write_string(&LABEL_MEMBER, "my-bucket")
1587        }
1588    }
1589
1590    #[test]
1591    fn http_label_substitution() {
1592        let request = make_protocol()
1593            .serialize_request(
1594                &LabelStruct,
1595                &LABEL_SCHEMA,
1596                "https://example.com/{bucketName}/objects",
1597                &ConfigBag::base(),
1598            )
1599            .unwrap();
1600        assert_eq!(request.uri(), "https://example.com/my-bucket/objects");
1601    }
1602
1603    #[test]
1604    fn http_label_percent_encodes() {
1605        struct SpecialLabelStruct;
1606        impl SerializableStruct for SpecialLabelStruct {
1607            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1608                s.write_string(&LABEL_MEMBER, "my bucket/name")
1609            }
1610        }
1611        let request = make_protocol()
1612            .serialize_request(
1613                &SpecialLabelStruct,
1614                &LABEL_SCHEMA,
1615                "https://example.com/{bucketName}",
1616                &ConfigBag::base(),
1617            )
1618            .unwrap();
1619        assert!(request.uri().contains("my%20bucket%2Fname"));
1620    }
1621
1622    static INT_LABEL_MEMBER: Schema = Schema::new_member(
1623        crate::shape_id!("test", "S"),
1624        ShapeType::Integer,
1625        "itemId",
1626        0,
1627    )
1628    .with_http_label();
1629
1630    static INT_LABEL_SCHEMA: Schema = Schema::new_struct(
1631        crate::shape_id!("test", "S"),
1632        ShapeType::Structure,
1633        &[&INT_LABEL_MEMBER],
1634    );
1635
1636    struct IntLabelStruct;
1637    impl SerializableStruct for IntLabelStruct {
1638        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1639            s.write_integer(&INT_LABEL_MEMBER, 123)
1640        }
1641    }
1642
1643    #[test]
1644    fn http_label_integer() {
1645        let request = make_protocol()
1646            .serialize_request(
1647                &IntLabelStruct,
1648                &INT_LABEL_SCHEMA,
1649                "https://example.com/items/{itemId}",
1650                &ConfigBag::base(),
1651            )
1652            .unwrap();
1653        assert_eq!(request.uri(), "https://example.com/items/123");
1654    }
1655
1656    // -- Combined: @httpHeader + @httpQuery + @httpLabel + body --
1657
1658    static COMBINED_LABEL: Schema =
1659        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "id", 0)
1660            .with_http_label();
1661    static COMBINED_HEADER: Schema =
1662        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "token", 1)
1663            .with_http_header("X-Token");
1664    static COMBINED_QUERY: Schema = Schema::new_member(
1665        crate::shape_id!("test", "S"),
1666        ShapeType::String,
1667        "filter",
1668        2,
1669    )
1670    .with_http_query("filter");
1671    static COMBINED_BODY: Schema =
1672        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "data", 3);
1673    static COMBINED_SCHEMA: Schema = Schema::new_struct(
1674        crate::shape_id!("test", "S"),
1675        ShapeType::Structure,
1676        &[
1677            &COMBINED_LABEL,
1678            &COMBINED_HEADER,
1679            &COMBINED_QUERY,
1680            &COMBINED_BODY,
1681        ],
1682    );
1683
1684    struct CombinedStruct;
1685    impl SerializableStruct for CombinedStruct {
1686        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1687            s.write_string(&COMBINED_LABEL, "item-42")?;
1688            s.write_string(&COMBINED_HEADER, "secret")?;
1689            s.write_string(&COMBINED_QUERY, "active")?;
1690            s.write_string(&COMBINED_BODY, "payload-data")
1691        }
1692    }
1693
1694    #[test]
1695    fn combined_bindings() {
1696        let request = make_protocol()
1697            .serialize_request(
1698                &CombinedStruct,
1699                &COMBINED_SCHEMA,
1700                "https://example.com/{id}/details",
1701                &ConfigBag::base(),
1702            )
1703            .unwrap();
1704        assert_eq!(
1705            request.uri(),
1706            "https://example.com/item-42/details?filter=active"
1707        );
1708        // Header
1709        assert_eq!(request.headers().get("X-Token").unwrap(), "secret");
1710        // Body contains only the unbound member
1711        let body = request.body().bytes().unwrap();
1712        assert!(body
1713            .windows(b"payload-data".len())
1714            .any(|w| w == b"payload-data"));
1715    }
1716
1717    // -- @httpPrefixHeaders tests --
1718
1719    static PREFIX_MEMBER: Schema =
1720        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Map, "metadata", 0)
1721            .with_http_prefix_headers("X-Meta-");
1722
1723    static PREFIX_SCHEMA: Schema = Schema::new_struct(
1724        crate::shape_id!("test", "S"),
1725        ShapeType::Structure,
1726        &[&PREFIX_MEMBER],
1727    );
1728
1729    struct PrefixHeaderStruct;
1730    impl SerializableStruct for PrefixHeaderStruct {
1731        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1732            s.write_map(&PREFIX_MEMBER, &|s| {
1733                s.write_string(&STRING, "Color")?;
1734                s.write_string(&STRING, "red")?;
1735                s.write_string(&STRING, "Size")?;
1736                s.write_string(&STRING, "large")?;
1737                Ok(())
1738            })
1739        }
1740    }
1741
1742    #[test]
1743    fn http_prefix_headers() {
1744        let request = make_protocol()
1745            .serialize_request(
1746                &PrefixHeaderStruct,
1747                &PREFIX_SCHEMA,
1748                "https://example.com",
1749                &ConfigBag::base(),
1750            )
1751            .unwrap();
1752        assert_eq!(request.headers().get("X-Meta-Color").unwrap(), "red");
1753        assert_eq!(request.headers().get("X-Meta-Size").unwrap(), "large");
1754    }
1755
1756    // -- @httpQueryParams tests --
1757
1758    static QUERY_PARAMS_MEMBER: Schema =
1759        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Map, "params", 0)
1760            .with_http_query_params();
1761
1762    static QUERY_PARAMS_SCHEMA: Schema = Schema::new_struct(
1763        crate::shape_id!("test", "S"),
1764        ShapeType::Structure,
1765        &[&QUERY_PARAMS_MEMBER],
1766    );
1767
1768    struct QueryParamsStruct;
1769    impl SerializableStruct for QueryParamsStruct {
1770        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1771            s.write_map(&QUERY_PARAMS_MEMBER, &|s| {
1772                s.write_string(&STRING, "page")?;
1773                s.write_string(&STRING, "2")?;
1774                s.write_string(&STRING, "limit")?;
1775                s.write_string(&STRING, "50")?;
1776                Ok(())
1777            })
1778        }
1779    }
1780
1781    #[test]
1782    fn http_query_params() {
1783        let request = make_protocol()
1784            .serialize_request(
1785                &QueryParamsStruct,
1786                &QUERY_PARAMS_SCHEMA,
1787                "https://example.com",
1788                &ConfigBag::base(),
1789            )
1790            .unwrap();
1791        assert_eq!(request.uri(), "https://example.com?page=2&limit=50");
1792    }
1793
1794    // -- Timestamp in header defaults to http-date --
1795
1796    static TS_HEADER_MEMBER: Schema = Schema::new_member(
1797        crate::shape_id!("test", "S"),
1798        ShapeType::Timestamp,
1799        "ifModified",
1800        0,
1801    )
1802    .with_http_header("If-Modified-Since");
1803
1804    static TS_HEADER_SCHEMA: Schema = Schema::new_struct(
1805        crate::shape_id!("test", "S"),
1806        ShapeType::Structure,
1807        &[&TS_HEADER_MEMBER],
1808    );
1809
1810    struct TimestampHeaderStruct;
1811    impl SerializableStruct for TimestampHeaderStruct {
1812        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1813            s.write_timestamp(&TS_HEADER_MEMBER, &aws_smithy_types::DateTime::from_secs(0))
1814        }
1815    }
1816
1817    #[test]
1818    fn timestamp_header_uses_http_date() {
1819        let request = make_protocol()
1820            .serialize_request(
1821                &TimestampHeaderStruct,
1822                &TS_HEADER_SCHEMA,
1823                "https://example.com",
1824                &ConfigBag::base(),
1825            )
1826            .unwrap();
1827        let value = request.headers().get("If-Modified-Since").unwrap();
1828        // http-date format: "Thu, 01 Jan 1970 00:00:00 GMT"
1829        assert!(value.contains("1970"), "expected http-date, got: {value}");
1830    }
1831
1832    // -- Timestamp in query defaults to date-time --
1833
1834    static TS_QUERY_MEMBER: Schema = Schema::new_member(
1835        crate::shape_id!("test", "S"),
1836        ShapeType::Timestamp,
1837        "since",
1838        0,
1839    )
1840    .with_http_query("since");
1841
1842    static TS_QUERY_SCHEMA: Schema = Schema::new_struct(
1843        crate::shape_id!("test", "S"),
1844        ShapeType::Structure,
1845        &[&TS_QUERY_MEMBER],
1846    );
1847
1848    struct TimestampQueryStruct;
1849    impl SerializableStruct for TimestampQueryStruct {
1850        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1851            s.write_timestamp(&TS_QUERY_MEMBER, &aws_smithy_types::DateTime::from_secs(0))
1852        }
1853    }
1854
1855    #[test]
1856    fn timestamp_query_uses_date_time() {
1857        let request = make_protocol()
1858            .serialize_request(
1859                &TimestampQueryStruct,
1860                &TS_QUERY_SCHEMA,
1861                "https://example.com",
1862                &ConfigBag::base(),
1863            )
1864            .unwrap();
1865        assert_eq!(
1866            request.uri(),
1867            "https://example.com?since=1970-01-01T00%3A00%3A00Z"
1868        );
1869    }
1870
1871    // -- Unbound members go to body, bound members do not --
1872
1873    static BOUND_MEMBER: Schema = Schema::new_member(
1874        crate::shape_id!("test", "S"),
1875        ShapeType::String,
1876        "headerVal",
1877        0,
1878    )
1879    .with_http_header("X-Val");
1880    static UNBOUND_MEMBER: Schema = Schema::new_member(
1881        crate::shape_id!("test", "S"),
1882        ShapeType::String,
1883        "bodyVal",
1884        1,
1885    );
1886    static MIXED_SCHEMA: Schema = Schema::new_struct(
1887        crate::shape_id!("test", "S"),
1888        ShapeType::Structure,
1889        &[&BOUND_MEMBER, &UNBOUND_MEMBER],
1890    );
1891
1892    struct MixedStruct;
1893    impl SerializableStruct for MixedStruct {
1894        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1895            s.write_string(&BOUND_MEMBER, "in-header")?;
1896            s.write_string(&UNBOUND_MEMBER, "in-body")
1897        }
1898    }
1899
1900    #[test]
1901    fn bound_members_not_in_body() {
1902        let request = make_protocol()
1903            .serialize_request(
1904                &MixedStruct,
1905                &MIXED_SCHEMA,
1906                "https://example.com",
1907                &ConfigBag::base(),
1908            )
1909            .unwrap();
1910        let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
1911        assert!(
1912            body.contains("in-body"),
1913            "body should contain unbound member"
1914        );
1915        assert!(
1916            !body.contains("in-header"),
1917            "body should NOT contain header-bound member"
1918        );
1919        assert_eq!(request.headers().get("X-Val").unwrap(), "in-header");
1920    }
1921}