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