Skip to main content

aws_smithy_schema/schema/http_protocol/
response.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! The composite response deserializer that makes a REST protocol the sole owner of
7//! response deserialization.
8//!
9//! A REST protocol returns one of these instead of a bare body deserializer. It reads
10//! `@httpHeader`, `@httpPrefixHeaders`, `@httpResponseCode`, and `@httpPayload` members from
11//! the HTTP message itself, delegates everything else to the protocol's body codec, and
12//! drives the same generated member-index consumer either way. Body-only protocols (RPC,
13//! Query, CBOR) keep returning their codec's deserializer and therefore ignore HTTP binding
14//! traits, as the SEP requires.
15//!
16//! # Where the decisions live
17//!
18//! Which members are bound comes from the const-derived routing word on the structure schema,
19//! so the common case visits only bound members rather than scanning. How each bound value
20//! parses lives in [`super::bound_value`]. This module is the router between them, plus the
21//! `NonUtf8HeaderHandling` policy and the body/transport precedence rule.
22
23use super::binding::HTTP_HEADER_NAME;
24use super::bound_value::{
25    HeaderValues, HttpHeaderValueDeserializer, HttpPrefixHeadersDeserializer,
26    HttpRawPayloadDeserializer, HttpStatusDeserializer,
27};
28use crate::serde::{SerdeError, ShapeDeserializer};
29use crate::{Schema, ShapeType};
30use aws_smithy_runtime_api::http::{Headers, NonUtf8HeaderHandling, Response};
31use aws_smithy_types::config_bag::ConfigBag;
32use aws_smithy_types::error::display::DisplayErrorContext;
33use aws_smithy_types::{BigDecimal, BigInteger, Blob, DateTime, Document};
34
35/// Reads the non-UTF-8 header policy out of a request's config, defaulting to
36/// [`NonUtf8HeaderHandling::Reject`].
37pub(crate) fn non_utf8_policy(cfg: &ConfigBag) -> NonUtf8HeaderHandling {
38    cfg.load::<NonUtf8HeaderHandling>()
39        .cloned()
40        .unwrap_or_default()
41}
42
43/// Where the composite gets its [`NonUtf8HeaderHandling`] from.
44///
45/// The policy only matters once a bound header has failed to parse *and* holds an unreadable
46/// value, which almost never happens. A [`ConfigBag`] load walks every layer, and doing it up
47/// front was a measurable share of a small response, so the production path keeps the bag and
48/// reads it only at that point — the same order the generated legacy parser used.
49#[derive(Clone)]
50pub(crate) enum NonUtf8Policy<'a> {
51    /// Already known, so no bag needs to be kept.
52    Resolved(NonUtf8HeaderHandling),
53    /// Read from the request's config only when a decision is needed.
54    Deferred(&'a ConfigBag),
55}
56
57impl NonUtf8Policy<'_> {
58    #[cold]
59    fn resolve(&self) -> NonUtf8HeaderHandling {
60        match self {
61            NonUtf8Policy::Resolved(policy) => policy.clone(),
62            NonUtf8Policy::Deferred(cfg) => non_utf8_policy(cfg),
63        }
64    }
65}
66
67impl From<NonUtf8HeaderHandling> for NonUtf8Policy<'_> {
68    fn from(policy: NonUtf8HeaderHandling) -> Self {
69        NonUtf8Policy::Resolved(policy)
70    }
71}
72
73impl<'a> From<&'a ConfigBag> for NonUtf8Policy<'a> {
74    fn from(cfg: &'a ConfigBag) -> Self {
75        NonUtf8Policy::Deferred(cfg)
76    }
77}
78
79/// Wraps a protocol's body deserializer so a **successful** response's transport-bound members
80/// are read from the HTTP message.
81///
82/// Every REST construction site must go through this or [`http_error_deserializer`], for two
83/// reasons. It is the single place binding behavior is decided, so a protocol wrapper cannot
84/// accidentally return a bare body deserializer and silently drop header members. And it is
85/// generic over the concrete body deserializer, so the composite is built *before* the result
86/// is coerced to `Box<dyn ShapeDeserializer>` — exactly one allocation per response, matching
87/// what the body-only path already costs. Boxing the body first and wrapping the box would add
88/// a second.
89///
90/// # The body-only fast path
91///
92/// When the output structure has no response-bound members but does have body members, the
93/// composite has nothing to contribute and the body deserializer is returned directly. This
94/// keeps body-only REST outputs off the composite entirely.
95///
96/// Both halves of the condition matter. An *empty* output has no body members either, and for
97/// it the composite is the better answer: it skips the codec instead of asking the codec to
98/// accept an empty body. A non-structure schema — such as
99/// [`prelude::DOCUMENT`](crate::prelude::DOCUMENT) — carries a zeroed routing word, so it
100/// reports no body members and also routes through the composite rather than being mistaken
101/// for a body-only output.
102pub fn http_output_deserializer<'a, D>(
103    body: D,
104    response: &'a Response,
105    output_schema: &Schema<'_>,
106    cfg: &'a ConfigBag,
107) -> Box<dyn ShapeDeserializer + 'a>
108where
109    D: ShapeDeserializer + 'a,
110{
111    if is_body_only(output_schema) {
112        return Box::new(body);
113    }
114    Box::new(HttpResponseDeserializer::for_output(
115        body,
116        response.headers(),
117        response.status().as_u16(),
118        response.body().bytes(),
119        cfg,
120    ))
121}
122
123/// Wraps a protocol's body deserializer so a **modeled error** response's transport-bound
124/// members are read from the HTTP message.
125///
126/// Separate from [`http_output_deserializer`] rather than a mode flag, because the two differ
127/// in ways that must not be reachable from the wrong call site:
128///
129/// - An error tolerates an empty body while still populating header and status members, which a
130///   successful response must not do — the codec's empty-body strictness is the current
131///   behavior there.
132/// - There is no error schema to pass. Which error variant is being built is not known until
133///   the generated variant calls `read_struct`, so the body-only fast path cannot be evaluated.
134///   Taking no schema makes that structural: an error path has nothing to apply the fast path
135///   to, so it cannot bypass the composite.
136pub fn http_error_deserializer<'a, D>(
137    body: D,
138    response: &'a Response,
139    cfg: &'a ConfigBag,
140) -> Box<dyn ShapeDeserializer + 'a>
141where
142    D: ShapeDeserializer + 'a,
143{
144    Box::new(HttpResponseDeserializer::for_error(
145        body,
146        response.headers(),
147        response.status().as_u16(),
148        response.body().bytes(),
149        cfg,
150    ))
151}
152
153/// True when the schema's members all belong to the body, so the composite would only add
154/// indirection.
155///
156/// Requires an exact routing word: a structure whose bindings sit beyond the mask reports
157/// `needs_scan`, and its mask is then meaningless rather than empty.
158fn is_body_only(schema: &Schema<'_>) -> bool {
159    !schema.response_bindings_need_scan()
160        && schema.response_binding_mask() == 0
161        && schema.has_response_body_members()
162}
163
164/// Deserializes a complete HTTP response: transport-bound members plus the protocol body.
165///
166/// Generic over the body deserializer so a protocol can construct this with its concrete
167/// codec type and box the result once. Boxing the body separately would add an allocation to
168/// every response.
169pub(crate) struct HttpResponseDeserializer<'a, D> {
170    body: D,
171    headers: &'a Headers,
172    status: u16,
173    /// The buffered body, or `None` when the body is still a live stream.
174    ///
175    /// A streaming response leaves the body unbuffered; its payload member is `@streaming` and
176    /// is owned by the generated streaming path, so nothing here needs the bytes.
177    body_bytes: Option<&'a [u8]>,
178    non_utf8: NonUtf8Policy<'a>,
179    /// Whether an empty body is acceptable instead of being handed to the codec.
180    ///
181    /// True only for modeled errors. Services send empty error bodies — S3's `HEAD` responses,
182    /// for instance — and those responses must still populate header and status members.
183    allow_empty_body: bool,
184}
185
186/// What a bound member turned out to be, so the caller knows whether the body codec still has
187/// the outer structure to read.
188#[derive(PartialEq, Eq)]
189enum Routed {
190    /// An ordinary binding: header, prefix map, or status.
191    Binding,
192    /// A `@httpPayload` member. The body belongs to this member, so the outer structure must
193    /// not also be read from it.
194    Payload,
195}
196
197impl<'a, D> HttpResponseDeserializer<'a, D>
198where
199    D: ShapeDeserializer,
200{
201    /// Builds a deserializer for a successful response.
202    pub(crate) fn for_output(
203        body: D,
204        headers: &'a Headers,
205        status: u16,
206        body_bytes: Option<&'a [u8]>,
207        non_utf8: impl Into<NonUtf8Policy<'a>>,
208    ) -> Self {
209        Self {
210            body,
211            headers,
212            status,
213            body_bytes,
214            non_utf8: non_utf8.into(),
215            allow_empty_body: false,
216        }
217    }
218
219    /// Builds a deserializer for a modeled error response, which tolerates an empty body.
220    pub(crate) fn for_error(
221        body: D,
222        headers: &'a Headers,
223        status: u16,
224        body_bytes: Option<&'a [u8]>,
225        non_utf8: impl Into<NonUtf8Policy<'a>>,
226    ) -> Self {
227        Self {
228            body,
229            headers,
230            status,
231            body_bytes,
232            non_utf8: non_utf8.into(),
233            allow_empty_body: true,
234        }
235    }
236
237    /// True when `Skip` applies, i.e. the policy is `Skip` *and* some raw value really is
238    /// unreadable.
239    ///
240    /// The unreadable check is what keeps `Skip` from hiding ordinary parse failures: a
241    /// malformed but valid-UTF-8 value still fails.
242    fn skips(&self, has_unreadable_value: bool) -> bool {
243        // Order matters: the policy is read only once a value really is unreadable.
244        has_unreadable_value && self.non_utf8.resolve() == NonUtf8HeaderHandling::Skip
245    }
246
247    /// Routes one response-bound member to its source.
248    #[inline]
249    fn route(
250        &mut self,
251        member: &Schema<'_>,
252        consumer: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
253    ) -> Result<Routed, SerdeError> {
254        if let Some(header) = member.http_header() {
255            self.route_header(member, header.value(), consumer)?;
256            return Ok(Routed::Binding);
257        }
258        if let Some(prefix) = member.http_prefix_headers() {
259            self.route_prefix(member, prefix.value(), consumer)?;
260            return Ok(Routed::Binding);
261        }
262        if member.http_response_code().is_some() {
263            // Unconditional: a response always has a status, so this member always has a
264            // value. That matches the generated path, which assigns it with no guard.
265            let mut status = HttpStatusDeserializer::new(self.status);
266            consumer(member, &mut status)?;
267            return Ok(Routed::Binding);
268        }
269        debug_assert!(
270            member.http_payload().is_some(),
271            "route is only called for response-bound members"
272        );
273        self.route_payload(member, consumer)?;
274        Ok(Routed::Payload)
275    }
276
277    #[inline]
278    fn route_header(
279        &mut self,
280        member: &Schema<'_>,
281        name: &str,
282        consumer: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
283    ) -> Result<(), SerdeError> {
284        // The name parsed once and cached on the member, shared with request
285        // serialization. A name that does not parse is looked up as given, which finds
286        // nothing.
287        let values = match member.extension(&HTTP_HEADER_NAME) {
288            Some(parsed) => HeaderValues::by_name(self.headers, parsed),
289            None => HeaderValues::new(self.headers, name),
290        };
291        if !values.is_present() {
292            // An absent header leaves the member absent. Invoking the consumer would make it
293            // produce a value.
294            return Ok(());
295        }
296        let mut value = HttpHeaderValueDeserializer::new(values);
297        if value.parses_to_no_value(member) {
298            // Present but yielding nothing to parse. The generated path reports this as an
299            // absent member, and no `read_*` return type can express it.
300            return Ok(());
301        }
302        match consumer(member, &mut value) {
303            Ok(()) => Ok(()),
304            Err(err) if self.skips(values.has_unreadable_value()) => {
305                // Leave the member absent. The header itself is untouched, so an interceptor
306                // can still read the original octets.
307                let _ = err;
308                Ok(())
309            }
310            Err(err) => Err(header_error(member, "header", name, err)),
311        }
312    }
313
314    // Out of line so `route` stays small for the common header case.
315    #[inline(never)]
316    fn route_prefix(
317        &mut self,
318        member: &Schema<'_>,
319        prefix: &str,
320        consumer: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
321    ) -> Result<(), SerdeError> {
322        // Invoked even when nothing matches: an absent prefix is `Some(empty_map)`, not `None`.
323        let mut map = HttpPrefixHeadersDeserializer::new(self.headers, prefix);
324        match consumer(member, &mut map) {
325            Ok(()) => Ok(()),
326            Err(err) if self.skips(map.has_unreadable_value()) => {
327                // The whole member is dropped rather than exposing a partial map.
328                let _ = err;
329                Ok(())
330            }
331            Err(err) => Err(header_error(member, "prefix header", prefix, err)),
332        }
333    }
334
335    #[inline(never)]
336    fn route_payload(
337        &mut self,
338        member: &Schema<'_>,
339        consumer: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
340    ) -> Result<(), SerdeError> {
341        if member.streaming() {
342            // The generated streaming path owns the live body or event receiver and installs
343            // it on the builder itself. Invoking the consumer here would overwrite it.
344            return Ok(());
345        }
346        let Some(body_bytes) = self.body_bytes else {
347            // Not buffered, yet not `@streaming`. Nothing can be read, and silently producing
348            // an absent member would hide a protocol wiring mistake.
349            return Err(SerdeError::invalid_input(format!(
350                "member `{}` has a non-streaming @httpPayload but the response body was not buffered",
351                member_name(member)
352            )));
353        };
354        if body_bytes.is_empty() {
355            // An empty payload leaves the member absent, for every payload kind. The generated
356            // path guards each assignment with `if !body.is_empty()`.
357            return Ok(());
358        }
359        match member.shape_type() {
360            // Raw payloads bypass the codec: the body *is* the value.
361            ShapeType::Blob | ShapeType::String => {
362                let mut raw = HttpRawPayloadDeserializer::new(body_bytes);
363                consumer(member, &mut raw)
364            }
365            // Everything else is a structured payload, read by the protocol's codec with the
366            // body root as the member's root.
367            _ => consumer(member, &mut self.body),
368        }
369    }
370}
371
372/// Builds the contextual error for a failed binding, naming the member and the header.
373///
374/// The parser's chain is already inside `err`'s message; [`DisplayErrorContext`] also picks up
375/// any source [`SerdeError`] carries. Without this context a caller sees only "failed to parse
376/// input as i32" with no indication of which header produced it.
377#[cold]
378fn header_error(member: &Schema<'_>, kind: &str, name: &str, err: SerdeError) -> SerdeError {
379    SerdeError::invalid_input(format!(
380        "failed to parse {} from {kind} `{name}`: {}",
381        member_name(member),
382        DisplayErrorContext(&err),
383    ))
384}
385
386fn member_name<'d>(member: &Schema<'d>) -> &'d str {
387    member.member_name().unwrap_or("<unnamed member>")
388}
389
390impl<D> ShapeDeserializer for HttpResponseDeserializer<'_, D>
391where
392    D: ShapeDeserializer,
393{
394    fn read_struct(
395        &mut self,
396        schema: &Schema<'_>,
397        consumer: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
398    ) -> Result<(), SerdeError> {
399        // Transport bindings are read first, then the body. The body pass skips any field that
400        // corresponds to a bound member, so the transport value is authoritative and a
401        // malformed body copy cannot overwrite it.
402        let mut has_payload = false;
403        if schema.response_bindings_need_scan() {
404            // A bound member sits beyond the mask's reach, so the mask is incomplete and must
405            // be ignored entirely.
406            for member in schema.members() {
407                if member.has_http_response_binding()
408                    && self.route(member, consumer)? == Routed::Payload
409                {
410                    has_payload = true;
411                }
412            }
413        } else {
414            let mut mask = schema.response_binding_mask();
415            while mask != 0 {
416                let index = mask.trailing_zeros() as usize;
417                mask &= mask - 1;
418                let member = schema
419                    .member_schema_by_index(index)
420                    .expect("the mask is derived from this schema's own member array");
421                if self.route(member, consumer)? == Routed::Payload {
422                    has_payload = true;
423                }
424            }
425        }
426
427        if has_payload {
428            // The body belongs to the payload member; there is no outer document to read.
429            return Ok(());
430        }
431        if !schema.has_response_body_members() {
432            // Every member is transport-bound, or there are none. Invoking the codec would
433            // make an empty or absent body an error for no reason.
434            return Ok(());
435        }
436        if self.allow_empty_body && self.body_bytes.map(|b| b.is_empty()).unwrap_or(true) {
437            // A modeled error with an empty body still populated its header and status
438            // members above; handing an empty body to the codec would fail the response.
439            return Ok(());
440        }
441
442        // Filter the body pass so a field for a transport-bound member is consumed and
443        // discarded rather than assigned.
444        self.body.read_struct(schema, &mut |member, deser| {
445            // The index-less callback is how JSON and CBOR report an unknown union variant;
446            // filtering it would hide those. Only a real modeled member can be bound.
447            if member.member_index().is_some() && member.has_http_response_binding() {
448                deser.skip_value()
449            } else {
450                consumer(member, deser)
451            }
452        })
453    }
454
455    // Everything below is delegation: once a member has been routed, any nested value is the
456    // body codec's or a bound-value deserializer's to read. These are reached only when a
457    // caller uses the composite where a non-structure was expected, which for a response means
458    // the body root.
459
460    fn read_list(
461        &mut self,
462        schema: &Schema<'_>,
463        consumer: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
464    ) -> Result<(), SerdeError> {
465        self.body.read_list(schema, consumer)
466    }
467
468    fn read_map(
469        &mut self,
470        schema: &Schema<'_>,
471        consumer: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
472    ) -> Result<(), SerdeError> {
473        self.body.read_map(schema, consumer)
474    }
475
476    fn read_boolean(&mut self, schema: &Schema<'_>) -> Result<bool, SerdeError> {
477        self.body.read_boolean(schema)
478    }
479
480    fn read_byte(&mut self, schema: &Schema<'_>) -> Result<i8, SerdeError> {
481        self.body.read_byte(schema)
482    }
483
484    fn read_short(&mut self, schema: &Schema<'_>) -> Result<i16, SerdeError> {
485        self.body.read_short(schema)
486    }
487
488    fn read_integer(&mut self, schema: &Schema<'_>) -> Result<i32, SerdeError> {
489        self.body.read_integer(schema)
490    }
491
492    fn read_long(&mut self, schema: &Schema<'_>) -> Result<i64, SerdeError> {
493        self.body.read_long(schema)
494    }
495
496    fn read_float(&mut self, schema: &Schema<'_>) -> Result<f32, SerdeError> {
497        self.body.read_float(schema)
498    }
499
500    fn read_double(&mut self, schema: &Schema<'_>) -> Result<f64, SerdeError> {
501        self.body.read_double(schema)
502    }
503
504    fn read_big_integer(&mut self, schema: &Schema<'_>) -> Result<BigInteger, SerdeError> {
505        self.body.read_big_integer(schema)
506    }
507
508    fn read_big_decimal(&mut self, schema: &Schema<'_>) -> Result<BigDecimal, SerdeError> {
509        self.body.read_big_decimal(schema)
510    }
511
512    fn read_string(&mut self, schema: &Schema<'_>) -> Result<String, SerdeError> {
513        self.body.read_string(schema)
514    }
515
516    fn read_blob(&mut self, schema: &Schema<'_>) -> Result<Blob, SerdeError> {
517        self.body.read_blob(schema)
518    }
519
520    fn read_timestamp(&mut self, schema: &Schema<'_>) -> Result<DateTime, SerdeError> {
521        self.body.read_timestamp(schema)
522    }
523
524    fn read_document(&mut self, schema: &Schema<'_>) -> Result<Document, SerdeError> {
525        self.body.read_document(schema)
526    }
527
528    fn is_null(&self) -> bool {
529        self.body.is_null()
530    }
531
532    fn read_null(&mut self) -> Result<(), SerdeError> {
533        self.body.read_null()
534    }
535
536    fn skip_value(&mut self) -> Result<(), SerdeError> {
537        self.body.skip_value()
538    }
539
540    fn container_size(&self) -> Option<usize> {
541        self.body.container_size()
542    }
543
544    fn read_string_list(&mut self, schema: &Schema<'_>) -> Result<Vec<String>, SerdeError> {
545        self.body.read_string_list(schema)
546    }
547
548    fn read_blob_list(&mut self, schema: &Schema<'_>) -> Result<Vec<Blob>, SerdeError> {
549        self.body.read_blob_list(schema)
550    }
551
552    fn read_integer_list(&mut self, schema: &Schema<'_>) -> Result<Vec<i32>, SerdeError> {
553        self.body.read_integer_list(schema)
554    }
555
556    fn read_long_list(&mut self, schema: &Schema<'_>) -> Result<Vec<i64>, SerdeError> {
557        self.body.read_long_list(schema)
558    }
559
560    fn read_string_string_map(
561        &mut self,
562        schema: &Schema<'_>,
563    ) -> Result<std::collections::HashMap<String, String>, SerdeError> {
564        self.body.read_string_string_map(schema)
565    }
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571    use crate::document::DocumentShapeDeserializer;
572    use crate::traits::TimestampFormat;
573    use crate::{shape_id, ShapeId};
574    use std::cell::RefCell;
575    use std::collections::HashMap;
576
577    const ID: ShapeId<'static> = shape_id!("test", "S");
578
579    fn headers(pairs: &[(&str, &[u8])]) -> Headers {
580        let mut map = http::HeaderMap::new();
581        for (name, value) in pairs {
582            map.append(
583                http::HeaderName::from_bytes(name.as_bytes()).expect("valid header name"),
584                http::HeaderValue::from_bytes(value).expect("valid header value"),
585            );
586        }
587        Headers::try_from(map).expect("valid headers")
588    }
589
590    /// What the generated member-index consumer would have been handed, keyed by member name.
591    #[derive(Default, Debug, PartialEq)]
592    struct Populated {
593        strings: HashMap<String, String>,
594        integers: HashMap<String, i32>,
595        lists: HashMap<String, Vec<String>>,
596        maps: HashMap<String, HashMap<String, String>>,
597        blobs: HashMap<String, Vec<u8>>,
598        timestamps: HashMap<String, i64>,
599        floats: HashMap<String, f32>,
600    }
601
602    /// A consumer that dispatches on the member's modeled type, the way a generated arm does.
603    ///
604    /// The real generated consumer matches on `member_index()`; dispatching on shape type keeps
605    /// these tests from needing a per-shape fixture while exercising the same call pattern.
606    fn consume(
607        out: &mut Populated,
608    ) -> impl FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError> + '_ {
609        move |member, deser| {
610            let name = member.member_name().unwrap_or("?").to_string();
611            match member.shape_type() {
612                ShapeType::String => {
613                    out.strings.insert(name, deser.read_string(member)?);
614                }
615                ShapeType::Integer => {
616                    out.integers.insert(name, deser.read_integer(member)?);
617                }
618                ShapeType::Timestamp => {
619                    out.timestamps
620                        .insert(name, deser.read_timestamp(member)?.secs());
621                }
622                ShapeType::Blob => {
623                    out.blobs
624                        .insert(name, deser.read_blob(member)?.into_inner());
625                }
626                ShapeType::Float => {
627                    out.floats.insert(name, deser.read_float(member)?);
628                }
629                ShapeType::List => {
630                    out.lists.insert(name, deser.read_string_list(member)?);
631                }
632                ShapeType::Map => {
633                    out.maps.insert(name, deser.read_string_string_map(member)?);
634                }
635                other => {
636                    return Err(SerdeError::unsupported(format!(
637                        "test consumer does not handle {other:?}"
638                    )))
639                }
640            }
641            Ok(())
642        }
643    }
644
645    /// What the stub body observed. Recorded through a shared `RefCell` because the composite
646    /// takes the body deserializer by value — there is deliberately no
647    /// `impl ShapeDeserializer for &mut T`, so a test cannot hold the body aside and inspect it.
648    #[derive(Default, Debug)]
649    struct BodyLog {
650        /// Whether `read_struct` was reached at all.
651        read: bool,
652        /// Members whose value the composite's filter consumed via `skip_value`, in body order.
653        skipped: Vec<String>,
654        /// The shape name of the schema `read_struct` was called with.
655        ///
656        /// For a structured `@httpPayload` this is decided by the generated arm, not by the
657        /// composite, which is what makes member-level `@xmlName` root naming a codegen concern.
658        root: Option<String>,
659    }
660
661    /// A body deserializer that reports a fixed set of members, so a test can put a copy of a
662    /// transport-bound member in the "body" and observe what the filter does with it.
663    #[derive(Debug)]
664    struct StubBody<'s> {
665        members: &'s [&'s Schema<'s>],
666        log: &'s RefCell<BodyLog>,
667    }
668
669    impl<'s> StubBody<'s> {
670        fn new(members: &'s [&'s Schema<'s>], log: &'s RefCell<BodyLog>) -> Self {
671            Self { members, log }
672        }
673    }
674
675    /// The marker a stub body puts in every value it produces, so a test can tell a value that
676    /// came from the body apart from one that came from the transport.
677    const FROM_BODY: &str = "from-body";
678
679    impl ShapeDeserializer for StubBody<'_> {
680        fn read_struct(
681            &mut self,
682            schema: &Schema<'_>,
683            consumer: &mut dyn FnMut(
684                &Schema<'_>,
685                &mut dyn ShapeDeserializer,
686            ) -> Result<(), SerdeError>,
687        ) -> Result<(), SerdeError> {
688            {
689                let mut log = self.log.borrow_mut();
690                log.read = true;
691                log.root = Some(schema.shape_id().shape_name().to_string());
692            }
693            for member in self.members {
694                let mut value = StubValue {
695                    name: member.member_name().unwrap_or("?").to_string(),
696                    log: self.log,
697                };
698                consumer(member, &mut value)?;
699            }
700            Ok(())
701        }
702
703        fn read_list(
704            &mut self,
705            _schema: &Schema<'_>,
706            _consumer: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
707        ) -> Result<(), SerdeError> {
708            Ok(())
709        }
710
711        fn read_map(
712            &mut self,
713            _schema: &Schema<'_>,
714            _consumer: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
715        ) -> Result<(), SerdeError> {
716            Ok(())
717        }
718
719        fn read_boolean(&mut self, _s: &Schema<'_>) -> Result<bool, SerdeError> {
720            Ok(false)
721        }
722        fn read_byte(&mut self, _s: &Schema<'_>) -> Result<i8, SerdeError> {
723            Ok(0)
724        }
725        fn read_short(&mut self, _s: &Schema<'_>) -> Result<i16, SerdeError> {
726            Ok(0)
727        }
728        fn read_integer(&mut self, _s: &Schema<'_>) -> Result<i32, SerdeError> {
729            Ok(0)
730        }
731        fn read_long(&mut self, _s: &Schema<'_>) -> Result<i64, SerdeError> {
732            Ok(0)
733        }
734        fn read_float(&mut self, _s: &Schema<'_>) -> Result<f32, SerdeError> {
735            Ok(0.0)
736        }
737        fn read_double(&mut self, _s: &Schema<'_>) -> Result<f64, SerdeError> {
738            Ok(0.0)
739        }
740        fn read_big_integer(&mut self, _s: &Schema<'_>) -> Result<BigInteger, SerdeError> {
741            Err(SerdeError::unsupported("stub"))
742        }
743        fn read_big_decimal(&mut self, _s: &Schema<'_>) -> Result<BigDecimal, SerdeError> {
744            Err(SerdeError::unsupported("stub"))
745        }
746        fn read_string(&mut self, _s: &Schema<'_>) -> Result<String, SerdeError> {
747            Ok(FROM_BODY.to_string())
748        }
749        fn read_blob(&mut self, _s: &Schema<'_>) -> Result<Blob, SerdeError> {
750            Ok(Blob::new(FROM_BODY.as_bytes().to_vec()))
751        }
752        fn read_timestamp(&mut self, _s: &Schema<'_>) -> Result<DateTime, SerdeError> {
753            Ok(DateTime::from_secs(0))
754        }
755        fn read_document(&mut self, _s: &Schema<'_>) -> Result<Document, SerdeError> {
756            Ok(Document::Null)
757        }
758        fn is_null(&self) -> bool {
759            false
760        }
761        fn container_size(&self) -> Option<usize> {
762            None
763        }
764    }
765
766    /// One member's value inside [`StubBody`], which records a `skip_value` call.
767    #[derive(Debug)]
768    struct StubValue<'s> {
769        name: String,
770        log: &'s RefCell<BodyLog>,
771    }
772
773    impl ShapeDeserializer for StubValue<'_> {
774        fn read_struct(
775            &mut self,
776            _schema: &Schema<'_>,
777            _consumer: &mut dyn FnMut(
778                &Schema<'_>,
779                &mut dyn ShapeDeserializer,
780            ) -> Result<(), SerdeError>,
781        ) -> Result<(), SerdeError> {
782            Ok(())
783        }
784        fn read_list(
785            &mut self,
786            _schema: &Schema<'_>,
787            _consumer: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
788        ) -> Result<(), SerdeError> {
789            Ok(())
790        }
791        fn read_map(
792            &mut self,
793            _schema: &Schema<'_>,
794            _consumer: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
795        ) -> Result<(), SerdeError> {
796            Ok(())
797        }
798        fn read_boolean(&mut self, _s: &Schema<'_>) -> Result<bool, SerdeError> {
799            Ok(true)
800        }
801        fn read_byte(&mut self, _s: &Schema<'_>) -> Result<i8, SerdeError> {
802            Ok(-9)
803        }
804        fn read_short(&mut self, _s: &Schema<'_>) -> Result<i16, SerdeError> {
805            Ok(-9)
806        }
807        fn read_integer(&mut self, _s: &Schema<'_>) -> Result<i32, SerdeError> {
808            Ok(-999)
809        }
810        fn read_long(&mut self, _s: &Schema<'_>) -> Result<i64, SerdeError> {
811            Ok(-999)
812        }
813        fn read_float(&mut self, _s: &Schema<'_>) -> Result<f32, SerdeError> {
814            Ok(-9.0)
815        }
816        fn read_double(&mut self, _s: &Schema<'_>) -> Result<f64, SerdeError> {
817            Ok(-9.0)
818        }
819        fn read_big_integer(&mut self, _s: &Schema<'_>) -> Result<BigInteger, SerdeError> {
820            Err(SerdeError::unsupported("stub"))
821        }
822        fn read_big_decimal(&mut self, _s: &Schema<'_>) -> Result<BigDecimal, SerdeError> {
823            Err(SerdeError::unsupported("stub"))
824        }
825        fn read_string(&mut self, _s: &Schema<'_>) -> Result<String, SerdeError> {
826            Ok(FROM_BODY.to_string())
827        }
828        fn read_blob(&mut self, _s: &Schema<'_>) -> Result<Blob, SerdeError> {
829            Ok(Blob::new(FROM_BODY.as_bytes().to_vec()))
830        }
831        fn read_timestamp(&mut self, _s: &Schema<'_>) -> Result<DateTime, SerdeError> {
832            Ok(DateTime::from_secs(-1))
833        }
834        fn read_document(&mut self, _s: &Schema<'_>) -> Result<Document, SerdeError> {
835            Ok(Document::Null)
836        }
837        fn is_null(&self) -> bool {
838            false
839        }
840        fn skip_value(&mut self) -> Result<(), SerdeError> {
841            self.log.borrow_mut().skipped.push(self.name.clone());
842            Ok(())
843        }
844        fn container_size(&self) -> Option<usize> {
845            None
846        }
847    }
848
849    // -- schemas --
850
851    static NAME_HEADER: Schema<'static> =
852        Schema::new_member(ID, ShapeType::String, "name", 0).with_http_header("x-name");
853    static COUNT_HEADER: Schema<'static> =
854        Schema::new_member(ID, ShapeType::Integer, "count", 1).with_http_header("x-count");
855    static STATUS: Schema<'static> =
856        Schema::new_member(ID, ShapeType::Integer, "status", 2).with_http_response_code();
857    static BODY_FIELD: Schema<'static> = Schema::new_member(ID, ShapeType::String, "note", 3);
858
859    static MIXED: Schema<'static> = Schema::new_struct(
860        ID,
861        ShapeType::Structure,
862        &[&NAME_HEADER, &COUNT_HEADER, &STATUS, &BODY_FIELD],
863    );
864
865    /// Every member is transport-bound, so the body codec has nothing to read.
866    static BOUND_NAME: Schema<'static> =
867        Schema::new_member(ID, ShapeType::String, "name", 0).with_http_header("x-name");
868    static BOUND_STATUS: Schema<'static> =
869        Schema::new_member(ID, ShapeType::Integer, "status", 1).with_http_response_code();
870    static FULLY_BOUND: Schema<'static> =
871        Schema::new_struct(ID, ShapeType::Structure, &[&BOUND_NAME, &BOUND_STATUS]);
872
873    static EMPTY_STRUCT: Schema<'static> = Schema::new_struct(ID, ShapeType::Structure, &[]);
874
875    static BODY_ONLY_FIELD: Schema<'static> = Schema::new_member(ID, ShapeType::String, "note", 0);
876    static BODY_ONLY: Schema<'static> =
877        Schema::new_struct(ID, ShapeType::Structure, &[&BODY_ONLY_FIELD]);
878
879    /// The success-path composite over a stub body, with the default Reject policy.
880    fn output<'a>(
881        members: &'a [&'a Schema<'a>],
882        log: &'a RefCell<BodyLog>,
883        h: &'a Headers,
884        status: u16,
885        body_bytes: Option<&'a [u8]>,
886    ) -> HttpResponseDeserializer<'a, StubBody<'a>> {
887        HttpResponseDeserializer::for_output(
888            StubBody::new(members, log),
889            h,
890            status,
891            body_bytes,
892            NonUtf8HeaderHandling::Reject,
893        )
894    }
895
896    const NO_BODY_MEMBERS: &[&Schema<'static>] = &[];
897
898    // ---------------------------------------------------------------------------------
899    // routing
900    // ---------------------------------------------------------------------------------
901
902    #[test]
903    fn headers_status_and_body_members_all_reach_the_consumer() {
904        let h = headers(&[("x-name", b"widget"), ("x-count", b"7")]);
905        let log = RefCell::new(BodyLog::default());
906        let body_members: &[&Schema<'_>] = &[&BODY_FIELD];
907        let mut out = Populated::default();
908        output(body_members, &log, &h, 201, Some(b"{}"))
909            .read_struct(&MIXED, &mut consume(&mut out))
910            .unwrap();
911        assert_eq!(out.strings.get("name"), Some(&"widget".to_string()));
912        assert_eq!(out.integers.get("count"), Some(&7));
913        assert_eq!(out.integers.get("status"), Some(&201));
914        assert_eq!(out.strings.get("note"), Some(&FROM_BODY.to_string()));
915        assert!(
916            log.borrow().read,
917            "the body codec must be invoked for body members"
918        );
919    }
920
921    #[test]
922    fn header_members_are_found_by_their_cached_names() {
923        // A mixed-case modeled name, a repeated header (re-read by name) and a modeled name
924        // that is not a valid header name, which has no cached name and finds nothing.
925        static MIXED_CASE: Schema<'static> =
926            Schema::new_member(ID, ShapeType::String, "md5", 0).with_http_header("Content-MD5");
927        static REPEATED: Schema<'static> = Schema::new_member(ID, ShapeType::List, "tags", 1)
928            .with_http_header("x-tags")
929            .with_list_member(&crate::prelude::STRING);
930        static INVALID: Schema<'static> =
931            Schema::new_member(ID, ShapeType::String, "bad", 2).with_http_header("bad name");
932        static NAMED: Schema<'static> = Schema::new_struct(
933            ID,
934            ShapeType::Structure,
935            &[&MIXED_CASE, &REPEATED, &INVALID],
936        );
937        assert!(MIXED_CASE.extension(&HTTP_HEADER_NAME).is_some());
938        assert!(INVALID.extension(&HTTP_HEADER_NAME).is_none());
939
940        let h = headers(&[
941            ("content-md5", b"abc=="),
942            ("x-tags", b"a"),
943            ("x-tags", b"b, c"),
944        ]);
945        let log = RefCell::new(BodyLog::default());
946        let mut out = Populated::default();
947        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
948            .read_struct(&NAMED, &mut consume(&mut out))
949            .unwrap();
950        assert_eq!(out.strings.get("md5"), Some(&"abc==".to_string()));
951        assert_eq!(
952            out.lists.get("tags"),
953            Some(&vec!["a".to_string(), "b".to_string(), "c".to_string()])
954        );
955        assert!(!out.strings.contains_key("bad"));
956    }
957
958    #[test]
959    fn an_absent_header_leaves_its_member_absent() {
960        let h = headers(&[]);
961        let log = RefCell::new(BodyLog::default());
962        let mut out = Populated::default();
963        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
964            .read_struct(&FULLY_BOUND, &mut consume(&mut out))
965            .unwrap();
966        assert!(
967            !out.strings.contains_key("name"),
968            "the consumer must not run for an absent header"
969        );
970        assert_eq!(out.integers.get("status"), Some(&200));
971    }
972
973    #[test]
974    fn a_present_but_valueless_header_leaves_its_member_absent() {
975        // The generated path reports an empty value as an absent member for everything except a
976        // plain string, and no `read_*` return type can express that.
977        let h = headers(&[("x-name", b""), ("x-count", b"")]);
978        let log = RefCell::new(BodyLog::default());
979        let body_members: &[&Schema<'_>] = &[&BODY_FIELD];
980        let mut out = Populated::default();
981        output(body_members, &log, &h, 200, Some(b"{}"))
982            .read_struct(&MIXED, &mut consume(&mut out))
983            .unwrap();
984        assert!(
985            !out.integers.contains_key("count"),
986            "an empty integer header must not become Some(0)"
987        );
988        assert_eq!(
989            out.strings.get("name"),
990            Some(&String::new()),
991            "an empty plain-string header is the empty string, not absent"
992        );
993    }
994
995    #[test]
996    fn the_body_codec_is_skipped_when_no_member_belongs_to_it() {
997        let h = headers(&[("x-name", b"widget")]);
998        let log = RefCell::new(BodyLog::default());
999        let mut out = Populated::default();
1000        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1001            .read_struct(&FULLY_BOUND, &mut consume(&mut out))
1002            .unwrap();
1003        assert!(
1004            !log.borrow().read,
1005            "a fully bound structure must not invoke the body codec"
1006        );
1007        assert_eq!(out.strings.get("name"), Some(&"widget".to_string()));
1008    }
1009
1010    #[test]
1011    fn an_empty_structure_invokes_nothing() {
1012        let h = headers(&[]);
1013        let log = RefCell::new(BodyLog::default());
1014        let mut out = Populated::default();
1015        output(NO_BODY_MEMBERS, &log, &h, 204, Some(b""))
1016            .read_struct(&EMPTY_STRUCT, &mut consume(&mut out))
1017            .unwrap();
1018        assert!(!log.borrow().read);
1019        assert_eq!(out, Populated::default());
1020    }
1021
1022    #[test]
1023    fn a_body_only_structure_goes_straight_to_the_codec() {
1024        let h = headers(&[]);
1025        let log = RefCell::new(BodyLog::default());
1026        let body_members: &[&Schema<'_>] = &[&BODY_ONLY_FIELD];
1027        let mut out = Populated::default();
1028        output(body_members, &log, &h, 200, Some(b"{}"))
1029            .read_struct(&BODY_ONLY, &mut consume(&mut out))
1030            .unwrap();
1031        assert!(log.borrow().read);
1032        assert_eq!(out.strings.get("note"), Some(&FROM_BODY.to_string()));
1033        assert!(log.borrow().skipped.is_empty());
1034    }
1035
1036    // ---------------------------------------------------------------------------------
1037    // transport wins over the body
1038    // ---------------------------------------------------------------------------------
1039
1040    #[test]
1041    fn a_body_copy_of_a_bound_member_is_consumed_and_discarded() {
1042        // The transport value is authoritative. A body field for the same member must be
1043        // consumed — so a cursor-based codec stays aligned — but must not overwrite it.
1044        let h = headers(&[("x-name", b"from-header"), ("x-count", b"7")]);
1045        let log = RefCell::new(BodyLog::default());
1046        let body_members: &[&Schema<'_>] = &[&NAME_HEADER, &COUNT_HEADER, &STATUS, &BODY_FIELD];
1047        let mut out = Populated::default();
1048        output(body_members, &log, &h, 201, Some(b"{}"))
1049            .read_struct(&MIXED, &mut consume(&mut out))
1050            .unwrap();
1051        assert_eq!(out.strings.get("name"), Some(&"from-header".to_string()));
1052        assert_eq!(out.integers.get("count"), Some(&7));
1053        assert_eq!(out.integers.get("status"), Some(&201));
1054        assert_eq!(
1055            log.borrow().skipped,
1056            vec![
1057                "name".to_string(),
1058                "count".to_string(),
1059                "status".to_string()
1060            ],
1061            "each bound member's body copy must be skipped, in body order"
1062        );
1063        assert_eq!(
1064            out.strings.get("note"),
1065            Some(&FROM_BODY.to_string()),
1066            "only the unbound member may come from the body"
1067        );
1068    }
1069
1070    #[test]
1071    fn an_index_less_callback_is_forwarded_rather_than_filtered() {
1072        // JSON and CBOR report an unknown union variant through a callback whose schema has no
1073        // member index. Filtering it would hide unknown variants.
1074        let body_members: &[&Schema<'_>] = &[&crate::prelude::DOCUMENT];
1075        let log = RefCell::new(BodyLog::default());
1076        let h = headers(&[]);
1077        let mut seen = Vec::new();
1078        output(body_members, &log, &h, 200, Some(b"{}"))
1079            .read_struct(&BODY_ONLY, &mut |member, _| {
1080                seen.push(member.member_index());
1081                Ok(())
1082            })
1083            .unwrap();
1084        assert_eq!(
1085            seen,
1086            vec![None],
1087            "the index-less callback must reach the consumer"
1088        );
1089        assert!(log.borrow().skipped.is_empty());
1090    }
1091
1092    // ---------------------------------------------------------------------------------
1093    // prefix headers
1094    // ---------------------------------------------------------------------------------
1095
1096    static PREFIX_KEY: Schema<'static> = Schema::new(shape_id!("test", "K"), ShapeType::String);
1097    static PREFIX_VALUE: Schema<'static> = Schema::new(shape_id!("test", "V"), ShapeType::String);
1098    static PREFIX_MEMBER: Schema<'static> = Schema::new_member(ID, ShapeType::Map, "meta", 0)
1099        .with_http_prefix_headers("x-meta-")
1100        .with_map_members(&PREFIX_KEY, &PREFIX_VALUE);
1101    static PREFIX_SCHEMA: Schema<'static> =
1102        Schema::new_struct(ID, ShapeType::Structure, &[&PREFIX_MEMBER]);
1103
1104    #[test]
1105    fn a_prefix_map_is_populated_from_matching_headers() {
1106        let h = headers(&[("x-meta-a", b"1"), ("x-other", b"2")]);
1107        let log = RefCell::new(BodyLog::default());
1108        let mut out = Populated::default();
1109        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1110            .read_struct(&PREFIX_SCHEMA, &mut consume(&mut out))
1111            .unwrap();
1112        assert_eq!(
1113            out.maps.get("meta").and_then(|m| m.get("a")),
1114            Some(&"1".to_string())
1115        );
1116    }
1117
1118    #[test]
1119    fn a_prefix_with_no_matches_is_an_empty_map_not_an_absent_member() {
1120        let h = headers(&[("x-other", b"2")]);
1121        let log = RefCell::new(BodyLog::default());
1122        let mut out = Populated::default();
1123        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1124            .read_struct(&PREFIX_SCHEMA, &mut consume(&mut out))
1125            .unwrap();
1126        assert_eq!(
1127            out.maps.get("meta"),
1128            Some(&HashMap::new()),
1129            "existing Smithy behavior is Some(empty_map)"
1130        );
1131    }
1132
1133    // ---------------------------------------------------------------------------------
1134    // payloads
1135    // ---------------------------------------------------------------------------------
1136
1137    static BLOB_PAYLOAD: Schema<'static> =
1138        Schema::new_member(ID, ShapeType::Blob, "payload", 0).with_http_payload();
1139    static PAYLOAD_HEADER: Schema<'static> =
1140        Schema::new_member(ID, ShapeType::String, "name", 1).with_http_header("x-name");
1141    static BLOB_PAYLOAD_SCHEMA: Schema<'static> =
1142        Schema::new_struct(ID, ShapeType::Structure, &[&BLOB_PAYLOAD, &PAYLOAD_HEADER]);
1143
1144    #[test]
1145    fn a_raw_payload_comes_from_the_body_bytes_and_bypasses_the_codec() {
1146        let h = headers(&[("x-name", b"widget")]);
1147        let log = RefCell::new(BodyLog::default());
1148        let body_members: &[&Schema<'_>] = &[&BODY_FIELD];
1149        let mut out = Populated::default();
1150        output(body_members, &log, &h, 200, Some(b"\x00\x01raw"))
1151            .read_struct(&BLOB_PAYLOAD_SCHEMA, &mut consume(&mut out))
1152            .unwrap();
1153        assert_eq!(
1154            out.blobs.get("payload").map(|b| b.as_slice()),
1155            Some(&b"\x00\x01raw"[..])
1156        );
1157        assert_eq!(out.strings.get("name"), Some(&"widget".to_string()));
1158        assert!(
1159            !log.borrow().read,
1160            "a payload member means there is no outer document for the codec to read"
1161        );
1162    }
1163
1164    #[test]
1165    fn an_empty_payload_leaves_the_member_absent() {
1166        let h = headers(&[("x-name", b"widget")]);
1167        let log = RefCell::new(BodyLog::default());
1168        let mut out = Populated::default();
1169        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1170            .read_struct(&BLOB_PAYLOAD_SCHEMA, &mut consume(&mut out))
1171            .unwrap();
1172        assert!(
1173            !out.blobs.contains_key("payload"),
1174            "an empty payload must not become an empty blob"
1175        );
1176        assert_eq!(out.strings.get("name"), Some(&"widget".to_string()));
1177    }
1178
1179    static STRUCT_PAYLOAD: Schema<'static> =
1180        Schema::new_member(ID, ShapeType::Structure, "payload", 0).with_http_payload();
1181    static STRUCT_PAYLOAD_SCHEMA: Schema<'static> =
1182        Schema::new_struct(ID, ShapeType::Structure, &[&STRUCT_PAYLOAD]);
1183
1184    #[test]
1185    fn a_structured_payload_is_handed_to_the_body_codec() {
1186        let h = headers(&[]);
1187        let log = RefCell::new(BodyLog::default());
1188        let mut saw_payload = false;
1189        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b"{}"))
1190            .read_struct(&STRUCT_PAYLOAD_SCHEMA, &mut |member, d| {
1191                assert_eq!(member.member_name(), Some("payload"));
1192                // The codec is positioned at the payload root, so a generated arm's
1193                // `Foo::deserialize(deser)` reads the body root as the member's value.
1194                d.read_struct(member, &mut |_, _| Ok(()))?;
1195                saw_payload = true;
1196                Ok(())
1197            })
1198            .unwrap();
1199        assert!(saw_payload);
1200        assert!(
1201            log.borrow().read,
1202            "the structured payload must be read through the body codec"
1203        );
1204    }
1205
1206    static STREAM_PAYLOAD: Schema<'static> = Schema::new_member(ID, ShapeType::Blob, "payload", 0)
1207        .with_http_payload()
1208        .with_streaming();
1209    static STREAM_SCHEMA: Schema<'static> = Schema::new_struct(
1210        ID,
1211        ShapeType::Structure,
1212        &[&STREAM_PAYLOAD, &PAYLOAD_HEADER],
1213    );
1214
1215    #[test]
1216    fn a_streaming_payload_member_is_never_handed_to_a_consumer() {
1217        // A streaming response leaves the body unbuffered, which is the realistic shape.
1218        let h = headers(&[("x-name", b"widget")]);
1219        let log = RefCell::new(BodyLog::default());
1220        let mut out = Populated::default();
1221        output(NO_BODY_MEMBERS, &log, &h, 200, None)
1222            .read_struct(&STREAM_SCHEMA, &mut consume(&mut out))
1223            .unwrap();
1224        assert!(
1225            !out.blobs.contains_key("payload"),
1226            "the generated streaming path owns the live body and installs it itself"
1227        );
1228        assert_eq!(
1229            out.strings.get("name"),
1230            Some(&"widget".to_string()),
1231            "header members must still be populated for a streaming output"
1232        );
1233        assert!(
1234            !log.borrow().read,
1235            "the live stream must not be parsed as a document"
1236        );
1237    }
1238
1239    #[test]
1240    fn a_non_streaming_payload_without_a_buffered_body_is_an_error() {
1241        // A wiring mistake rather than a service behavior, so reporting an absent member would
1242        // hide it.
1243        let h = headers(&[]);
1244        let log = RefCell::new(BodyLog::default());
1245        let err = output(NO_BODY_MEMBERS, &log, &h, 200, None)
1246            .read_struct(&BLOB_PAYLOAD_SCHEMA, &mut |_, _| Ok(()))
1247            .expect_err("no buffered body");
1248        assert!(
1249            format!("{err}").contains("was not buffered"),
1250            "unexpected error: {err}"
1251        );
1252    }
1253
1254    // ---------------------------------------------------------------------------------
1255    // empty bodies
1256    // ---------------------------------------------------------------------------------
1257
1258    #[test]
1259    fn a_modeled_error_with_an_empty_body_still_gets_its_headers_and_status() {
1260        // Services send empty error bodies; S3's HEAD responses are the standing example.
1261        let h = headers(&[("x-name", b"widget")]);
1262        let log = RefCell::new(BodyLog::default());
1263        let body_members: &[&Schema<'_>] = &[&BODY_FIELD];
1264        let mut out = Populated::default();
1265        HttpResponseDeserializer::for_error(
1266            StubBody::new(body_members, &log),
1267            &h,
1268            404,
1269            Some(b""),
1270            NonUtf8HeaderHandling::Reject,
1271        )
1272        .read_struct(&MIXED, &mut consume(&mut out))
1273        .unwrap();
1274        assert_eq!(out.strings.get("name"), Some(&"widget".to_string()));
1275        assert_eq!(out.integers.get("status"), Some(&404));
1276        assert!(
1277            !log.borrow().read,
1278            "an empty error body must not be handed to the codec"
1279        );
1280    }
1281
1282    #[test]
1283    fn a_modeled_error_with_a_body_still_reads_it() {
1284        let h = headers(&[]);
1285        let log = RefCell::new(BodyLog::default());
1286        let body_members: &[&Schema<'_>] = &[&BODY_ONLY_FIELD];
1287        let mut out = Populated::default();
1288        HttpResponseDeserializer::for_error(
1289            StubBody::new(body_members, &log),
1290            &h,
1291            400,
1292            Some(b"{}"),
1293            NonUtf8HeaderHandling::Reject,
1294        )
1295        .read_struct(&BODY_ONLY, &mut consume(&mut out))
1296        .unwrap();
1297        assert!(log.borrow().read);
1298        assert_eq!(out.strings.get("note"), Some(&FROM_BODY.to_string()));
1299    }
1300
1301    #[test]
1302    fn a_successful_response_keeps_the_codecs_empty_body_strictness() {
1303        // Only errors tolerate an empty body. A success must still reach the codec so its
1304        // existing behavior is preserved rather than decided here.
1305        let h = headers(&[]);
1306        let log = RefCell::new(BodyLog::default());
1307        let body_members: &[&Schema<'_>] = &[&BODY_ONLY_FIELD];
1308        let mut out = Populated::default();
1309        output(body_members, &log, &h, 200, Some(b""))
1310            .read_struct(&BODY_ONLY, &mut consume(&mut out))
1311            .unwrap();
1312        assert!(
1313            log.borrow().read,
1314            "the composite must not decide emptiness on behalf of a successful response"
1315        );
1316    }
1317
1318    // ---------------------------------------------------------------------------------
1319    // NonUtf8HeaderHandling
1320    // ---------------------------------------------------------------------------------
1321
1322    fn with_policy<'a>(
1323        h: &'a Headers,
1324        policy: NonUtf8HeaderHandling,
1325        log: &'a RefCell<BodyLog>,
1326    ) -> HttpResponseDeserializer<'a, StubBody<'a>> {
1327        HttpResponseDeserializer::for_output(
1328            StubBody::new(NO_BODY_MEMBERS, log),
1329            h,
1330            200,
1331            Some(b""),
1332            policy,
1333        )
1334    }
1335
1336    #[test]
1337    fn an_unreadable_header_is_rejected_by_default_with_member_and_header_context() {
1338        let h = headers(&[("x-name", b"value-\xe9")]);
1339        let log = RefCell::new(BodyLog::default());
1340        let mut out = Populated::default();
1341        let err = with_policy(&h, NonUtf8HeaderHandling::Reject, &log)
1342            .read_struct(&FULLY_BOUND, &mut consume(&mut out))
1343            .expect_err("non-UTF-8 must be rejected");
1344        let message = format!("{err}");
1345        for expected in ["name", "x-name", "not valid utf-8"] {
1346            assert!(
1347                message.contains(expected),
1348                "expected {expected:?} in {message:?}"
1349            );
1350        }
1351    }
1352
1353    #[test]
1354    fn skip_leaves_an_unreadable_member_absent_and_the_header_intact() {
1355        let h = headers(&[("x-name", b"value-\xe9")]);
1356        let log = RefCell::new(BodyLog::default());
1357        let mut out = Populated::default();
1358        with_policy(&h, NonUtf8HeaderHandling::Skip, &log)
1359            .read_struct(&FULLY_BOUND, &mut consume(&mut out))
1360            .expect("Skip must not fail the response");
1361        assert!(!out.strings.contains_key("name"));
1362        assert_eq!(out.integers.get("status"), Some(&200));
1363        assert_eq!(
1364            h.get_bytes("x-name"),
1365            Some(&b"value-\xe9"[..]),
1366            "the raw octets must stay readable for interceptors"
1367        );
1368    }
1369
1370    #[test]
1371    fn skip_does_not_hide_a_readable_but_malformed_value() {
1372        // The distinction that keeps Skip narrow: it applies to unreadable encodings only.
1373        let h = headers(&[("x-name", b"ok"), ("x-count", b"notanint")]);
1374        let log = RefCell::new(BodyLog::default());
1375        let mut out = Populated::default();
1376        let err = with_policy(&h, NonUtf8HeaderHandling::Skip, &log)
1377            .read_struct(&MIXED, &mut consume(&mut out))
1378            .expect_err("a malformed integer is still an error under Skip");
1379        assert!(format!("{err}").contains("x-count"), "unexpected: {err}");
1380    }
1381
1382    static LIST_ELEMENT: Schema<'static> = Schema::new(shape_id!("test", "E"), ShapeType::String);
1383    static LIST_HEADER: Schema<'static> = Schema::new_member(ID, ShapeType::List, "tags", 0)
1384        .with_http_header("x-tags")
1385        .with_list_member(&LIST_ELEMENT);
1386    static LIST_SCHEMA: Schema<'static> =
1387        Schema::new_struct(ID, ShapeType::Structure, &[&LIST_HEADER]);
1388
1389    #[test]
1390    fn skip_applies_to_the_whole_member_regardless_of_value_order() {
1391        // PR #4868's order-independence rule. List parsing stops at its first failure, so the
1392        // decision must come from scanning all raw values rather than from the error.
1393        for pairs in [
1394            vec![("x-tags", b"value-\xe9".as_slice()), ("x-tags", b"ok")],
1395            vec![("x-tags", b"ok".as_slice()), ("x-tags", b"value-\xe9")],
1396        ] {
1397            let h = headers(&pairs);
1398            let log = RefCell::new(BodyLog::default());
1399            let mut out = Populated::default();
1400            with_policy(&h, NonUtf8HeaderHandling::Skip, &log)
1401                .read_struct(&LIST_SCHEMA, &mut consume(&mut out))
1402                .expect("Skip must apply in both orders");
1403            assert!(!out.lists.contains_key("tags"));
1404        }
1405    }
1406
1407    #[test]
1408    fn a_list_header_is_populated_when_every_value_is_readable() {
1409        let h = headers(&[("x-tags", b"a,b"), ("x-tags", b"c")]);
1410        let log = RefCell::new(BodyLog::default());
1411        let mut out = Populated::default();
1412        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1413            .read_struct(&LIST_SCHEMA, &mut consume(&mut out))
1414            .unwrap();
1415        assert_eq!(
1416            out.lists.get("tags"),
1417            Some(&vec!["a".to_string(), "b".to_string(), "c".to_string()])
1418        );
1419    }
1420
1421    #[test]
1422    fn a_prefix_map_is_suppressed_whole_under_skip() {
1423        let h = headers(&[("x-meta-good", b"ok"), ("x-meta-bad", b"value-\xe9")]);
1424        let log = RefCell::new(BodyLog::default());
1425        let mut out = Populated::default();
1426        with_policy(&h, NonUtf8HeaderHandling::Skip, &log)
1427            .read_struct(&PREFIX_SCHEMA, &mut consume(&mut out))
1428            .expect("Skip must not fail the response");
1429        assert!(
1430            !out.maps.contains_key("meta"),
1431            "a partial map would conceal the dropped entry"
1432        );
1433    }
1434
1435    #[test]
1436    fn an_unreadable_prefix_entry_is_rejected_by_default_naming_the_prefix() {
1437        let h = headers(&[("x-meta-bad", b"value-\xe9")]);
1438        let log = RefCell::new(BodyLog::default());
1439        let mut out = Populated::default();
1440        let err = with_policy(&h, NonUtf8HeaderHandling::Reject, &log)
1441            .read_struct(&PREFIX_SCHEMA, &mut consume(&mut out))
1442            .expect_err("non-UTF-8 must be rejected");
1443        let message = format!("{err}");
1444        for expected in ["meta", "prefix header", "x-meta-"] {
1445            assert!(
1446                message.contains(expected),
1447                "expected {expected:?} in {message:?}"
1448            );
1449        }
1450    }
1451
1452    #[test]
1453    fn the_policy_defaults_to_reject_and_round_trips_through_the_config_bag() {
1454        assert_eq!(
1455            non_utf8_policy(&ConfigBag::base()),
1456            NonUtf8HeaderHandling::Reject,
1457            "an empty bag must read as Reject rather than requiring callers to default it"
1458        );
1459        let mut layer = aws_smithy_types::config_bag::CloneableLayer::new("test");
1460        layer.store_put(NonUtf8HeaderHandling::Skip);
1461        let bag = ConfigBag::of_layers(vec![layer.into()]);
1462        assert_eq!(non_utf8_policy(&bag), NonUtf8HeaderHandling::Skip);
1463    }
1464
1465    // ---------------------------------------------------------------------------------
1466    // mask vs scan
1467    // ---------------------------------------------------------------------------------
1468
1469    /// Builds a structure whose members are all header-bound, at runtime, to reach member
1470    /// counts that are impractical as statics.
1471    fn all_bound(count: usize) -> &'static Schema<'static> {
1472        let mut leaked: Vec<&'static Schema<'static>> = Vec::new();
1473        for index in 0..count {
1474            let name: &'static str = Box::leak(format!("m{index}").into_boxed_str());
1475            let header: &'static str = Box::leak(format!("x-m{index}").into_boxed_str());
1476            leaked.push(Box::leak(Box::new(
1477                Schema::new_member(ID, ShapeType::String, name, index).with_http_header(header),
1478            )));
1479        }
1480        let members: &'static [&'static Schema<'static>] = Box::leak(leaked.into_boxed_slice());
1481        Box::leak(Box::new(Schema::new_struct(
1482            ID,
1483            ShapeType::Structure,
1484            members,
1485        )))
1486    }
1487
1488    fn header_pairs(indices: impl Iterator<Item = usize>) -> Vec<(String, Vec<u8>)> {
1489        indices
1490            .map(|i| (format!("x-m{i}"), format!("v{i}").into_bytes()))
1491            .collect()
1492    }
1493
1494    #[test]
1495    fn the_scan_fallback_routes_the_same_members_as_the_mask() {
1496        // A bound member beyond the mask's reach forces the scan path; it must find all of them.
1497        let schema = all_bound(64);
1498        assert!(
1499            schema.response_bindings_need_scan(),
1500            "precondition: 64 bound members exceeds the mask"
1501        );
1502        let pairs = header_pairs(0..64);
1503        let borrowed: Vec<(&str, &[u8])> = pairs
1504            .iter()
1505            .map(|(n, v)| (n.as_str(), v.as_slice()))
1506            .collect();
1507        let h = headers(&borrowed);
1508        let log = RefCell::new(BodyLog::default());
1509        let mut out = Populated::default();
1510        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1511            .read_struct(schema, &mut consume(&mut out))
1512            .unwrap();
1513        assert_eq!(out.strings.len(), 64);
1514        assert_eq!(out.strings.get("m0"), Some(&"v0".to_string()));
1515        assert_eq!(out.strings.get("m63"), Some(&"v63".to_string()));
1516    }
1517
1518    #[test]
1519    fn the_mask_path_and_the_scan_path_agree_on_the_same_members() {
1520        // 62 bound members is the largest exact mask; 64 forces the scan. Both must populate
1521        // every member they share.
1522        let exact = all_bound(62);
1523        let scanned = all_bound(64);
1524        assert!(!exact.response_bindings_need_scan());
1525        assert!(scanned.response_bindings_need_scan());
1526
1527        let pairs = header_pairs(0..62);
1528        let borrowed: Vec<(&str, &[u8])> = pairs
1529            .iter()
1530            .map(|(n, v)| (n.as_str(), v.as_slice()))
1531            .collect();
1532        let h = headers(&borrowed);
1533
1534        let mut from_mask = Populated::default();
1535        let log = RefCell::new(BodyLog::default());
1536        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1537            .read_struct(exact, &mut consume(&mut from_mask))
1538            .unwrap();
1539
1540        let mut from_scan = Populated::default();
1541        let log = RefCell::new(BodyLog::default());
1542        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1543            .read_struct(scanned, &mut consume(&mut from_scan))
1544            .unwrap();
1545
1546        assert_eq!(
1547            from_mask, from_scan,
1548            "the two routing paths must produce identical results"
1549        );
1550        assert_eq!(from_mask.strings.len(), 62);
1551    }
1552
1553    #[test]
1554    fn a_sparse_binding_layout_visits_only_the_bound_members() {
1555        let mut leaked: Vec<&'static Schema<'static>> = Vec::new();
1556        for index in 0..50usize {
1557            let name: &'static str = Box::leak(format!("m{index}").into_boxed_str());
1558            let schema = if index % 12 == 0 {
1559                let header: &'static str = Box::leak(format!("x-m{index}").into_boxed_str());
1560                Schema::new_member(ID, ShapeType::String, name, index).with_http_header(header)
1561            } else {
1562                Schema::new_member(ID, ShapeType::String, name, index)
1563            };
1564            leaked.push(Box::leak(Box::new(schema)));
1565        }
1566        let members: &'static [&'static Schema<'static>] = Box::leak(leaked.into_boxed_slice());
1567        let schema = Schema::new_struct(ID, ShapeType::Structure, members);
1568        assert!(!schema.response_bindings_need_scan());
1569        assert_eq!(schema.response_binding_mask().count_ones(), 5);
1570
1571        let pairs = header_pairs((0..50).filter(|i| i % 12 == 0));
1572        let borrowed: Vec<(&str, &[u8])> = pairs
1573            .iter()
1574            .map(|(n, v)| (n.as_str(), v.as_slice()))
1575            .collect();
1576        let h = headers(&borrowed);
1577        let log = RefCell::new(BodyLog::default());
1578        let mut out = Populated::default();
1579        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b"{}"))
1580            .read_struct(&schema, &mut consume(&mut out))
1581            .unwrap();
1582        assert_eq!(out.strings.len(), 5);
1583        assert!(
1584            log.borrow().read,
1585            "the 45 unbound members still belong to the body"
1586        );
1587    }
1588
1589    static TS_HEADER: Schema<'static> = Schema::new_member(ID, ShapeType::Timestamp, "at", 0)
1590        .with_http_header("x-at")
1591        .with_timestamp_format(TimestampFormat::EpochSeconds);
1592    static TS_SCHEMA: Schema<'static> = Schema::new_struct(ID, ShapeType::Structure, &[&TS_HEADER]);
1593
1594    #[test]
1595    fn a_timestamp_header_uses_the_schemas_format() {
1596        let h = headers(&[("x-at", b"1445412480")]);
1597        let log = RefCell::new(BodyLog::default());
1598        let mut out = Populated::default();
1599        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1600            .read_struct(&TS_SCHEMA, &mut consume(&mut out))
1601            .unwrap();
1602        assert_eq!(out.timestamps.get("at"), Some(&1_445_412_480));
1603    }
1604
1605    // ---------------------------------------------------------------------------------
1606    // delegation
1607    // ---------------------------------------------------------------------------------
1608
1609    #[test]
1610    fn non_structure_reads_fall_through_to_the_body() {
1611        // Reached when a caller uses the composite where the body root is not a structure.
1612        let doc = Document::String("hello".to_string());
1613        let h = headers(&[]);
1614        let mut deser = HttpResponseDeserializer::for_output(
1615            DocumentShapeDeserializer::new(&doc),
1616            &h,
1617            200,
1618            Some(b""),
1619            NonUtf8HeaderHandling::Reject,
1620        );
1621        assert_eq!(deser.read_string(&crate::prelude::STRING).unwrap(), "hello");
1622    }
1623
1624    // ---------------------------------------------------------------------------------
1625    // value semantics, end to end through the composite
1626    //
1627    // The value deserializers are unit-tested directly in `bound_value`. These cases prove the
1628    // composite hands them the whole member — every raw value, and the member's own schema — so
1629    // parsing behavior that depends on either is not lost in routing. They mirror the rows of
1630    // the generated-path test `schema response header parsing matches legacy semantics`, which
1631    // is deleted when the generated parser is.
1632    // ---------------------------------------------------------------------------------
1633
1634    static NAN_HEADER: Schema<'static> =
1635        Schema::new_member(ID, ShapeType::Float, "nan", 0).with_http_header("x-nan");
1636    static INF_HEADER: Schema<'static> =
1637        Schema::new_member(ID, ShapeType::Float, "inf", 1).with_http_header("x-inf");
1638    static NEG_INF_HEADER: Schema<'static> =
1639        Schema::new_member(ID, ShapeType::Float, "neg", 2).with_http_header("x-neg");
1640    static FLOATS: Schema<'static> = Schema::new_struct(
1641        ID,
1642        ShapeType::Structure,
1643        &[&NAN_HEADER, &INF_HEADER, &NEG_INF_HEADER],
1644    );
1645
1646    #[test]
1647    fn smithy_special_float_values_survive_the_composite() {
1648        // Smithy spells these `NaN`/`Infinity`/`-Infinity` on the wire, which is not what
1649        // `f32::from_str` accepts. Reaching the consumer with the right values proves the
1650        // composite routes through the Smithy primitive parser rather than a plain `FromStr`.
1651        let h = headers(&[
1652            ("x-nan", b"NaN"),
1653            ("x-inf", b"Infinity"),
1654            ("x-neg", b"-Infinity"),
1655        ]);
1656        let log = RefCell::new(BodyLog::default());
1657        let mut out = Populated::default();
1658        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1659            .read_struct(&FLOATS, &mut consume(&mut out))
1660            .unwrap();
1661        assert!(out.floats["nan"].is_nan());
1662        assert_eq!(out.floats["inf"], f32::INFINITY);
1663        assert_eq!(out.floats["neg"], f32::NEG_INFINITY);
1664    }
1665
1666    static MEDIA_HEADER: Schema<'static> = Schema::new_member(ID, ShapeType::String, "config", 0)
1667        .with_http_header("x-config")
1668        .with_media_type("application/json");
1669    static MEDIA: Schema<'static> = Schema::new_struct(ID, ShapeType::Structure, &[&MEDIA_HEADER]);
1670
1671    #[test]
1672    fn a_media_type_header_is_base64_decoded_through_the_composite() {
1673        // `@mediaType` changes a string header from "take the value whole" to "base64-decode
1674        // it". The trait is on the member schema, so this only works if the composite passes
1675        // the member schema down rather than a prelude schema.
1676        let h = headers(&[("x-config", b"eyJhIjoxfQ==")]);
1677        let log = RefCell::new(BodyLog::default());
1678        let mut out = Populated::default();
1679        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1680            .read_struct(&MEDIA, &mut consume(&mut out))
1681            .unwrap();
1682        assert_eq!(out.strings.get("config"), Some(&r#"{"a":1}"#.to_string()));
1683    }
1684
1685    #[test]
1686    fn a_media_type_header_that_decodes_to_invalid_utf8_is_an_error_not_a_skip() {
1687        // The base64 is well-formed and every raw octet is valid UTF-8, so nothing about the
1688        // header is "unreadable" — the failure is in the decoded bytes. Skip must not suppress
1689        // it, otherwise a malformed payload becomes an absent member.
1690        let h = headers(&[("x-config", b"/w==")]); // decodes to 0xFF
1691        for policy in [NonUtf8HeaderHandling::Reject, NonUtf8HeaderHandling::Skip] {
1692            let log = RefCell::new(BodyLog::default());
1693            let mut out = Populated::default();
1694            let err = HttpResponseDeserializer::for_output(
1695                StubBody::new(NO_BODY_MEMBERS, &log),
1696                &h,
1697                200,
1698                Some(b""),
1699                policy.clone(),
1700            )
1701            .read_struct(&MEDIA, &mut consume(&mut out))
1702            .expect_err("invalid UTF-8 after base64 decoding is a parse error");
1703            let message = format!("{}", DisplayErrorContext(&err));
1704            assert!(
1705                message.contains("config") && message.contains("x-config"),
1706                "error must name the member and header, got: {message}"
1707            );
1708            assert!(
1709                !out.strings.contains_key("config"),
1710                "a failed parse must not populate the member"
1711            );
1712        }
1713    }
1714
1715    #[test]
1716    fn quoted_list_values_keep_their_commas_through_the_composite() {
1717        // Repeated lines are already covered by `a_list_header_is_populated_when_every_value_is_readable`.
1718        // This adds the RFC 7230 quoting rule, which a naive comma split would break.
1719        let h = headers(&[("x-tags", br#""a,b", c, "d\"e""#)]);
1720        let log = RefCell::new(BodyLog::default());
1721        let mut out = Populated::default();
1722        output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1723            .read_struct(&LIST_SCHEMA, &mut consume(&mut out))
1724            .unwrap();
1725        assert_eq!(
1726            out.lists.get("tags"),
1727            Some(&vec![
1728                "a,b".to_string(),
1729                "c".to_string(),
1730                "d\"e".to_string()
1731            ])
1732        );
1733    }
1734
1735    #[test]
1736    fn a_scalar_receiving_repeated_lines_is_a_cardinality_error() {
1737        // Not last-write-wins. The message is the parser's own, so the count reaches the caller
1738        // through the composite's context wrapper.
1739        let h = headers(&[("x-count", b"1"), ("x-count", b"2")]);
1740        let log = RefCell::new(BodyLog::default());
1741        let mut out = Populated::default();
1742        let err = output(NO_BODY_MEMBERS, &log, &h, 200, Some(b""))
1743            .read_struct(&MIXED, &mut consume(&mut out))
1744            .expect_err("two values for a scalar header is an error");
1745        let message = format!("{}", DisplayErrorContext(&err));
1746        assert!(
1747            message.contains("expected one item but found 2"),
1748            "the parser's cardinality message must survive, got: {message}"
1749        );
1750        assert!(
1751            message.contains("count") && message.contains("x-count"),
1752            "error must name the member and header, got: {message}"
1753        );
1754    }
1755
1756    // ---------------------------------------------------------------------------------
1757    // structured payload root
1758    // ---------------------------------------------------------------------------------
1759
1760    const PAYLOAD_ID: ShapeId<'static> = shape_id!("test", "Payload");
1761
1762    static XML_NAMED_PAYLOAD: Schema<'static> =
1763        Schema::new_member(ID, ShapeType::Structure, "payload", 0)
1764            .with_http_payload()
1765            .with_xml_name("CustomRoot");
1766    static XML_NAMED_OUTPUT: Schema<'static> =
1767        Schema::new_struct(ID, ShapeType::Structure, &[&XML_NAMED_PAYLOAD]);
1768
1769    static PAYLOAD_FIELD: Schema<'static> =
1770        Schema::new_member(PAYLOAD_ID, ShapeType::String, "inner", 0);
1771    static PAYLOAD_TARGET: Schema<'static> =
1772        Schema::new_struct(PAYLOAD_ID, ShapeType::Structure, &[&PAYLOAD_FIELD]);
1773
1774    #[test]
1775    fn a_structured_payload_root_is_chosen_by_the_consumer_not_the_composite() {
1776        // The composite positions the codec at the body root and hands the consumer the *member*
1777        // schema; the schema the codec actually sees as its root is whichever one the generated
1778        // arm forwards. The legacy generated arm forwards the target's schema
1779        // (`Target::deserialize(deser)`), so member-level `@xmlName` is not consulted for the
1780        // payload root — in either path. This pins that the composite does not change it.
1781        let h = headers(&[]);
1782        let log = RefCell::new(BodyLog::default());
1783        let body_members: &[&Schema<'_>] = &[&PAYLOAD_FIELD];
1784        let mut saw_member_xml_name = None;
1785        let mut inner = None;
1786        output(body_members, &log, &h, 200, Some(b"<CustomRoot/>"))
1787            .read_struct(&XML_NAMED_OUTPUT, &mut |member, deser| {
1788                saw_member_xml_name = member.xml_name().map(|n| n.value().to_string());
1789                // Exactly what the generated arm does: forward the *target's* schema.
1790                deser.read_struct(&PAYLOAD_TARGET, &mut |field, value| {
1791                    inner = Some((
1792                        field.member_name().unwrap_or("?").to_string(),
1793                        value.read_string(field)?,
1794                    ));
1795                    Ok(())
1796                })
1797            })
1798            .unwrap();
1799        assert_eq!(
1800            saw_member_xml_name.as_deref(),
1801            Some("CustomRoot"),
1802            "the member's traits must be reachable from the schema the composite passes"
1803        );
1804        assert_eq!(
1805            log.borrow().root.as_deref(),
1806            Some("Payload"),
1807            "the codec's root is the schema the consumer forwarded, not the member's xmlName"
1808        );
1809        assert_eq!(
1810            inner,
1811            Some(("inner".to_string(), FROM_BODY.to_string())),
1812            "the codec must be positioned at the payload root"
1813        );
1814    }
1815
1816    // ---------------------------------------------------------------------------------
1817    // the shared wrapping helpers
1818    //
1819    // These are the only sanctioned way for a REST protocol to build a response
1820    // deserializer, so their branch decisions are load-bearing: a wrong body-only verdict
1821    // silently drops every transport-bound member.
1822    // ---------------------------------------------------------------------------------
1823
1824    use aws_smithy_types::body::SdkBody;
1825
1826    fn http_response(status: u16, pairs: &[(&str, &[u8])], body: SdkBody) -> Response {
1827        let mut builder = http::Response::builder().status(status);
1828        for (name, value) in pairs {
1829            builder = builder.header(*name, http::HeaderValue::from_bytes(value).expect("value"));
1830        }
1831        Response::try_from(builder.body(body).expect("response")).expect("convertible")
1832    }
1833
1834    /// A body deserializer that reports one string member, so a test can tell whether the codec
1835    /// was reached without needing a real codec.
1836    fn one_member_body<'a>(
1837        members: &'a [&'a Schema<'a>],
1838        log: &'a RefCell<BodyLog>,
1839    ) -> StubBody<'a> {
1840        StubBody::new(members, log)
1841    }
1842
1843    #[test]
1844    fn a_body_only_output_takes_the_bare_body_fast_path() {
1845        assert!(
1846            is_body_only(&BODY_ONLY),
1847            "an output with only body members has nothing for the composite to do"
1848        );
1849        // The observable behavior is identical either way — that is the point of the fast path —
1850        // so the assertion above is the discriminator, and this half proves the returned
1851        // deserializer still works.
1852        let log = RefCell::new(BodyLog::default());
1853        let body_members: &[&Schema<'_>] = &[&BODY_ONLY_FIELD];
1854        let response = http_response(200, &[], SdkBody::from("{}"));
1855        let mut out = Populated::default();
1856        http_output_deserializer(
1857            one_member_body(body_members, &log),
1858            &response,
1859            &BODY_ONLY,
1860            &ConfigBag::base(),
1861        )
1862        .read_struct(&BODY_ONLY, &mut consume(&mut out))
1863        .unwrap();
1864        assert_eq!(out.strings.get("note"), Some(&FROM_BODY.to_string()));
1865    }
1866
1867    #[test]
1868    fn an_empty_output_does_not_take_the_fast_path() {
1869        // An empty output has no body members either. Handing it to the codec would make an
1870        // empty body an error for nothing, so it must route through the composite.
1871        assert!(!is_body_only(&EMPTY_STRUCT));
1872        let log = RefCell::new(BodyLog::default());
1873        let response = http_response(204, &[], SdkBody::empty());
1874        let mut out = Populated::default();
1875        http_output_deserializer(
1876            one_member_body(NO_BODY_MEMBERS, &log),
1877            &response,
1878            &EMPTY_STRUCT,
1879            &ConfigBag::base(),
1880        )
1881        .read_struct(&EMPTY_STRUCT, &mut consume(&mut out))
1882        .unwrap();
1883        assert!(
1884            !log.borrow().read,
1885            "the codec must not be invoked for an empty output"
1886        );
1887    }
1888
1889    #[test]
1890    fn a_bound_output_does_not_take_the_fast_path() {
1891        assert!(!is_body_only(&MIXED));
1892        let log = RefCell::new(BodyLog::default());
1893        let body_members: &[&Schema<'_>] = &[&BODY_FIELD];
1894        let response = http_response(201, &[("x-name", b"widget")], SdkBody::from("{}"));
1895        let mut out = Populated::default();
1896        http_output_deserializer(
1897            one_member_body(body_members, &log),
1898            &response,
1899            &MIXED,
1900            &ConfigBag::base(),
1901        )
1902        .read_struct(&MIXED, &mut consume(&mut out))
1903        .unwrap();
1904        assert_eq!(out.strings.get("name"), Some(&"widget".to_string()));
1905        assert_eq!(
1906            out.integers.get("status"),
1907            Some(&201),
1908            "the status must come from the response the helper was given"
1909        );
1910    }
1911
1912    #[test]
1913    fn a_non_structure_schema_does_not_take_the_fast_path() {
1914        // The default error forwarding passes `prelude::DOCUMENT`. Its routing word is zeroed,
1915        // which reports no body members, so it must not be mistaken for a body-only output.
1916        assert!(!is_body_only(&crate::prelude::DOCUMENT));
1917    }
1918
1919    /// No error path can reach the success body-only fast path, whatever schema it is later
1920    /// handed.
1921    ///
1922    /// The fast path exists so a REST output with no response bindings costs nothing, but it
1923    /// returns the *bare* codec — a deserializer that cannot read a header, a prefix map or a
1924    /// status. Applying it to an error would silently drop every bound error member, and the
1925    /// concrete error schema is not even known when the deserializer is built: the generated
1926    /// error variant supplies it later, when it calls `read_struct`.
1927    ///
1928    /// [`http_error_deserializer`] therefore takes no schema at all, which makes the property
1929    /// structural rather than a rule to remember. This test pins the consequence by choosing the
1930    /// one schema most likely to defeat it — a genuinely body-only one, for which the success
1931    /// helper *does* take the fast path on identical inputs.
1932    #[test]
1933    fn the_error_path_never_takes_the_body_only_fast_path() {
1934        assert!(
1935            is_body_only(&BODY_ONLY),
1936            "precondition: this schema is the fast path's best case"
1937        );
1938        let body_members: &[&Schema<'_>] = &[&BODY_ONLY_FIELD];
1939        let response = || http_response(404, &[], SdkBody::empty());
1940
1941        // Error mode: the composite owns the empty-body decision and skips the codec.
1942        let error_log = RefCell::new(BodyLog::default());
1943        let mut out = Populated::default();
1944        http_error_deserializer(
1945            one_member_body(body_members, &error_log),
1946            &response(),
1947            &ConfigBag::base(),
1948        )
1949        .read_struct(&BODY_ONLY, &mut consume(&mut out))
1950        .unwrap();
1951        assert!(
1952            !error_log.borrow().read,
1953            "an error must go through the composite even for a body-only schema"
1954        );
1955
1956        // Success mode on the same inputs: the fast path hands the body straight to the codec.
1957        // This half is what gives the assertion above its teeth — it proves the two modes really
1958        // do diverge here, so the error result is not simply what any deserializer would do.
1959        let output_log = RefCell::new(BodyLog::default());
1960        let mut out = Populated::default();
1961        http_output_deserializer(
1962            one_member_body(body_members, &output_log),
1963            &response(),
1964            &BODY_ONLY,
1965            &ConfigBag::base(),
1966        )
1967        .read_struct(&BODY_ONLY, &mut consume(&mut out))
1968        .unwrap();
1969        assert!(
1970            output_log.borrow().read,
1971            "precondition: the success helper takes the fast path for this schema"
1972        );
1973    }
1974
1975    #[test]
1976    fn a_binding_beyond_the_mask_still_defeats_the_fast_path() {
1977        // The case the `needs_scan` guard exists for: member 63 is bound, but 63 is outside the
1978        // mask, so the mask reads as zero. Without the guard this would look body-only and the
1979        // binding would be silently dropped.
1980        let mut leaked: Vec<&'static Schema<'static>> = Vec::new();
1981        for index in 0..64usize {
1982            let name: &'static str = Box::leak(format!("m{index}").into_boxed_str());
1983            let schema = if index == 63 {
1984                Schema::new_member(ID, ShapeType::String, name, index).with_http_header("x-late")
1985            } else {
1986                Schema::new_member(ID, ShapeType::String, name, index)
1987            };
1988            leaked.push(Box::leak(Box::new(schema)));
1989        }
1990        let members: &'static [&'static Schema<'static>] = Box::leak(leaked.into_boxed_slice());
1991        let schema: &'static Schema<'static> = Box::leak(Box::new(Schema::new_struct(
1992            ID,
1993            ShapeType::Structure,
1994            members,
1995        )));
1996        assert_eq!(
1997            schema.response_binding_mask(),
1998            0,
1999            "precondition: the only binding is outside the mask"
2000        );
2001        assert!(schema.response_bindings_need_scan());
2002        assert!(schema.has_response_body_members());
2003        assert!(
2004            !is_body_only(schema),
2005            "an inexact routing word must never be read as body-only"
2006        );
2007
2008        let log = RefCell::new(BodyLog::default());
2009        let response = http_response(200, &[("x-late", b"found")], SdkBody::from("{}"));
2010        let mut out = Populated::default();
2011        http_output_deserializer(
2012            one_member_body(NO_BODY_MEMBERS, &log),
2013            &response,
2014            schema,
2015            &ConfigBag::base(),
2016        )
2017        .read_struct(schema, &mut consume(&mut out))
2018        .unwrap();
2019        assert_eq!(out.strings.get("m63"), Some(&"found".to_string()));
2020    }
2021
2022    #[test]
2023    fn the_error_helper_tolerates_an_empty_body_while_still_reading_bindings() {
2024        // What separates the two helpers. A modeled error with an empty body — an S3 `HEAD`, for
2025        // instance — must still populate its header and status members.
2026        let log = RefCell::new(BodyLog::default());
2027        let body_members: &[&Schema<'_>] = &[&BODY_FIELD];
2028        let response = http_response(404, &[("x-name", b"missing")], SdkBody::empty());
2029        let mut out = Populated::default();
2030        http_error_deserializer(
2031            one_member_body(body_members, &log),
2032            &response,
2033            &ConfigBag::base(),
2034        )
2035        .read_struct(&MIXED, &mut consume(&mut out))
2036        .unwrap();
2037        assert_eq!(out.strings.get("name"), Some(&"missing".to_string()));
2038        assert_eq!(out.integers.get("status"), Some(&404));
2039        assert!(
2040            !log.borrow().read,
2041            "an empty error body must not be handed to the codec"
2042        );
2043    }
2044
2045    #[test]
2046    fn the_success_helper_keeps_the_codecs_empty_body_strictness() {
2047        // The same response through the output helper still reaches the codec, preserving
2048        // whatever the codec does with an empty body today.
2049        let log = RefCell::new(BodyLog::default());
2050        let body_members: &[&Schema<'_>] = &[&BODY_FIELD];
2051        let response = http_response(200, &[("x-name", b"here")], SdkBody::empty());
2052        let mut out = Populated::default();
2053        http_output_deserializer(
2054            one_member_body(body_members, &log),
2055            &response,
2056            &MIXED,
2057            &ConfigBag::base(),
2058        )
2059        .read_struct(&MIXED, &mut consume(&mut out))
2060        .unwrap();
2061        assert!(
2062            log.borrow().read,
2063            "a successful response must still hand an empty body to the codec"
2064        );
2065    }
2066
2067    #[test]
2068    fn the_helpers_read_the_policy_from_the_config_bag() {
2069        let response = http_response(200, &[("x-name", b"value-\xe9")], SdkBody::from("{}"));
2070        let mut cfg = ConfigBag::base();
2071        cfg.interceptor_state()
2072            .store_put(NonUtf8HeaderHandling::Skip);
2073
2074        for label in ["output", "error"] {
2075            let log = RefCell::new(BodyLog::default());
2076            let mut out = Populated::default();
2077            let mut deser = if label == "output" {
2078                http_output_deserializer(
2079                    one_member_body(NO_BODY_MEMBERS, &log),
2080                    &response,
2081                    &FULLY_BOUND,
2082                    &cfg,
2083                )
2084            } else {
2085                http_error_deserializer(one_member_body(NO_BODY_MEMBERS, &log), &response, &cfg)
2086            };
2087            deser
2088                .read_struct(&FULLY_BOUND, &mut consume(&mut out))
2089                .unwrap_or_else(|e| panic!("{label}: Skip must reach the composite: {e}"));
2090            assert!(
2091                !out.strings.contains_key("name"),
2092                "{label}: the unreadable member must be absent"
2093            );
2094        }
2095    }
2096
2097    #[test]
2098    fn an_unbuffered_body_is_reported_as_unbuffered_not_as_empty() {
2099        // The helper passes `bytes()` through as an `Option` rather than collapsing `None` into
2100        // `&[]`. That distinction is what lets a `@streaming` payload be skipped while a
2101        // non-streaming payload over an unbuffered body is reported as a wiring mistake instead
2102        // of silently becoming an absent member.
2103        let log = RefCell::new(BodyLog::default());
2104        let response = http_response(200, &[], SdkBody::taken());
2105        assert!(
2106            response.body().bytes().is_none(),
2107            "precondition: the body is not buffered"
2108        );
2109        let mut out = Populated::default();
2110        let err = http_output_deserializer(
2111            one_member_body(NO_BODY_MEMBERS, &log),
2112            &response,
2113            &BLOB_PAYLOAD_SCHEMA,
2114            &ConfigBag::base(),
2115        )
2116        .read_struct(&BLOB_PAYLOAD_SCHEMA, &mut consume(&mut out))
2117        .expect_err("a non-streaming payload cannot be read from an unbuffered body");
2118        assert!(
2119            format!("{err}").contains("was not buffered"),
2120            "unexpected: {err}"
2121        );
2122    }
2123}