Skip to main content

aws_smithy_schema/
lib.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6/* Automatically managed default lints */
7#![cfg_attr(docsrs, feature(doc_cfg))]
8/* End of automatically managed default lints */
9#![forbid(unsafe_code)]
10#![warn(
11    missing_docs,
12    rustdoc::missing_crate_level_docs,
13    missing_debug_implementations,
14    rust_2018_idioms,
15    unreachable_pub
16)]
17
18//! Runtime schema types for Smithy shapes.
19//!
20//! This crate provides the core types for representing Smithy schemas at
21//! runtime, enabling protocol-agnostic serialization and deserialization.
22//! The two central types are [`Schema`] (a runtime descriptor of a Smithy
23//! shape) and [`ShapeId`] (a Smithy shape identifier). Both are parameterized
24//! over a lifetime `'a` that names the data their string fields and member
25//! references borrow from.
26//!
27//! # API stability
28//!
29//! Items marked `#[doc(hidden)]` are not part of the public API. They exist for
30//! generated code and the `shape_id!` macro to call, and may change or disappear
31//! in any release.
32//!
33//! # Construction patterns
34//!
35//! ## `Schema<'static>` — the codegen-emitted form
36//!
37//! Generated SDK code emits every schema as a `static` of type
38//! `Schema<'static>`, built at compile time via `const fn` constructors and
39//! `with_*` setters. The Smithy prelude entries in this crate
40//! ([`prelude::STRING`], [`prelude::INTEGER`], etc.) follow the same pattern.
41//! Because the entire schema graph lives in the binary's data segment, there
42//! is no startup cost and no heap allocation on the hot serde path.
43//!
44//! ```
45//! use aws_smithy_schema::{shape_id, Schema, ShapeId, ShapeType};
46//!
47//! const SHAPE_ID: ShapeId<'static> = shape_id!("ns", "MyShape");
48//! static MY_SHAPE_SCHEMA: Schema<'static> = Schema::new(SHAPE_ID, ShapeType::String);
49//! assert_eq!(MY_SHAPE_SCHEMA.shape_id().as_str(), "ns#MyShape");
50//! ```
51//!
52//! Declare schemas as `static`s, not `const`s. A `const` is copied at every
53//! use, so each use gets its own empty [`extension`] cache, and the
54//! work cached on it is repeated.
55//!
56//! ## `Schema<'a>` — runtime construction
57//!
58//! Hand-written code may construct schemas at any lifetime; this is the path
59//! used by test fixtures and (in the future) by dynamic clients that load a
60//! Smithy model at runtime. Every value referenced by the schema must outlive
61//! the schema itself; the borrow checker enforces this. See
62//! [`Schema::new_struct`] for a worked example.
63//!
64//! Wire-format trait values follow the same rule and may borrow from storage
65//! the caller owns, such as an arena holding the parsed model text — with one
66//! exception. [`Schema::with_http_header`] requires `&'static str`, because the
67//! HTTP binder passes header names to an API that needs
68//! `Cow<'static, str>`. Use [`intern_header_name`] to obtain a suitable
69//! `&'static str` from a runtime string; it deduplicates, so the cost is
70//! bounded by the number of distinct header names in the model.
71//!
72//! # Trait maps and the `LazyLock` discipline
73//!
74//! The serde traits a schema cares about ([`@jsonName`][traits::JsonNameTrait],
75//! HTTP bindings, etc.) are stored as inline typed `Option` fields on the
76//! schema and accessed via direct field reads. Unknown or custom traits go
77//! through a fallback [`TraitMap`] reachable from [`Schema::with_traits`].
78//!
79//! [`Schema::with_traits`] accepts an `&'a std::sync::LazyLock<TraitMap>`, so
80//! the `LazyLock` must outlive any schema that references it. Codegen places
81//! both in statics (the `LazyLock` is `'static`, the schema is
82//! `Schema<'static>`); runtime-constructed schemas must arrange the lifetimes
83//! manually using standard borrow-check discipline.
84//!
85//! # Schema extensions
86//!
87//! Codecs and protocols often need data derived from a schema, such as a
88//! pre-encoded wire name or a member lookup table, that is the same on every
89//! request. [`Schema::extension`] computes such data once per schema and caches
90//! it on the schema, keyed by a [`SchemaExtensionKey`](extension::SchemaExtensionKey)
91//! that any crate can declare. Every schema carries the cache, whether it was
92//! generated, written by hand, or built from a model at runtime. See the
93//! [`extension`] module.
94//!
95//! Because of that cache, `Schema` has interior mutability and drop glue. The
96//! constructors and `with_*` setters remain `const fn`s, so schemas can still be
97//! initialized at compile time as `static`s, and a `const` can still refer to
98//! one (`const S: &Schema<'static> = &MY_STATIC;`). Two constant-evaluation
99//! patterns are not possible: a `const` item cannot call a method that reads a
100//! schema `static`, and a `const` or `static` cannot borrow a temporary
101//! `Schema` (`&Schema::new(..)`). Declare the schema as a `static` and borrow
102//! that instead.
103//!
104//! # Variance
105//!
106//! `Schema<'a>`, `ShapeId<'a>`, and the typed trait wrappers are covariant in
107//! `'a`. Covariance is what
108//! lets a `&'static Schema<'static>` (the codegen form) coerce implicitly
109//! into a `&Schema<'_>` argument at any call site, with no annotation needed
110//! at the call site. Compile-time assertion functions in this crate enforce
111//! covariance; if a future field change would break it, the build fails
112//! before any downstream code is affected.
113
114mod schema {
115    pub(crate) mod payload_hint;
116    pub(crate) mod shape_id;
117    pub(crate) mod shape_type;
118    pub(crate) mod trait_map;
119    pub(crate) mod trait_type;
120    pub(crate) mod traits;
121
122    pub(crate) mod codec;
123    pub(crate) mod document;
124    pub(crate) mod error_envelope;
125    pub(crate) mod extension;
126    pub(crate) mod header_omit_settings;
127    pub(crate) mod http_protocol;
128    pub(crate) mod prelude;
129    pub(crate) mod protocol;
130    pub(crate) mod registry;
131    pub(crate) mod serde;
132}
133
134pub use schema::payload_hint::PayloadHint;
135pub use schema::shape_id::ShapeId;
136pub use schema::shape_type::ShapeType;
137pub use schema::trait_map::TraitMap;
138pub use schema::trait_type::Trait;
139pub use schema::trait_type::{AnnotationTrait, DocumentTrait, StringTrait};
140
141/// Interns a header name so it can be attached to a runtime-materialized schema.
142///
143/// [`Schema::with_http_header`] is the one trait setter that requires
144/// `&'static str` rather than `&'a str`, because the HTTP binder hands header
145/// names to `Headers::insert`, which needs a `Cow<'static, str>`. Every other
146/// wire-format trait can borrow from storage the caller owns — an arena holding
147/// the parsed model text, for instance — and be dropped freely.
148///
149/// This function bridges that gap: it takes a runtime string and returns a
150/// `&'static str` suitable for [`Schema::with_http_header`], keeping the
151/// binder's zero-allocation fast path intact
152/// (`HttpHeaderTrait::value_static` returns `Some` for interned names).
153///
154/// # This leaks, deliberately
155///
156/// The returned `&'static str` is never freed. Interning is deduplicated, so a
157/// given name leaks at most once no matter how many times it is interned; the
158/// total is bounded by the number of *distinct* header names passed in, not by
159/// the number of calls. Re-materializing the same model repeatedly does not
160/// grow memory. For reference, the whole of Amazon S3 uses 158 distinct header
161/// names, about 4.4 KiB once.
162///
163/// Prefer this over a hand-rolled `Box::leak`, which leaks on every call and so
164/// grows without bound when placed in a loop.
165///
166/// # When this is the wrong tool
167///
168/// Because the memory is never reclaimed, the bound is "every distinct header
169/// name this process has ever seen". That is fine for a client that loads its
170/// models at startup. It is *not* fine for a long-lived process that keeps
171/// ingesting new models, where the total grows without limit and cannot be
172/// reclaimed. Such callers should not intern; they need `@httpHeader` names
173/// borrowed from their own storage, which is an additive relaxation of
174/// [`Schema::with_http_header`] rather than something an interner can provide.
175///
176/// # Examples
177///
178/// ```
179/// use aws_smithy_schema::intern_header_name;
180///
181/// // A header name that only exists at runtime.
182/// let parsed = String::from("x-amz-request-id");
183/// let name: &'static str = intern_header_name(&parsed);
184/// assert_eq!(name, "x-amz-request-id");
185///
186/// // Interning the same name again returns the identical pointer, not a copy.
187/// assert!(std::ptr::eq(name, intern_header_name("x-amz-request-id")));
188/// ```
189pub fn intern_header_name(name: &str) -> &'static str {
190    static INTERNED: std::sync::LazyLock<
191        std::sync::Mutex<std::collections::HashSet<&'static str>>,
192    > = std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashSet::new()));
193
194    // The guarded section cannot panic, so a poisoned lock is not reachable in
195    // practice; recover rather than propagate a panic if it somehow happens.
196    let mut table = INTERNED
197        .lock()
198        .unwrap_or_else(std::sync::PoisonError::into_inner);
199
200    if let Some(&existing) = table.get(name) {
201        return existing;
202    }
203    let leaked: &'static str = Box::leak(name.to_owned().into_boxed_str());
204    table.insert(leaked);
205    leaked
206}
207
208/// Schemas for the Smithy prelude shapes (`smithy.api#String`,
209/// `smithy.api#Integer`, and so on).
210pub mod prelude {
211    pub use crate::schema::prelude::*;
212}
213
214/// Derived data that runtime components compute once per schema and cache on it.
215pub mod extension {
216    pub use crate::schema::extension::SchemaExtensionKey;
217}
218
219/// Shape serialization and deserialization traits and their error type.
220pub mod serde {
221    pub use crate::schema::serde::*;
222}
223
224/// Runtime representations of the Smithy serialization traits carried on a schema.
225pub mod traits {
226    pub use crate::schema::traits::*;
227}
228
229/// Format codecs that pair a shape serializer with a matching deserializer.
230pub mod codec {
231    pub use crate::schema::codec::*;
232}
233
234/// `Document` shape serde and the discriminated-document conversion extension.
235pub mod document {
236    pub use crate::schema::document::*;
237}
238
239/// Settings controlling which HTTP headers are omitted during serialization.
240pub mod header_omit_settings {
241    pub use crate::schema::header_omit_settings::*;
242}
243
244/// Client protocol abstraction for serializing requests and deserializing responses.
245pub mod protocol {
246    pub use crate::schema::protocol::*;
247}
248
249/// Shared helpers for parsing AWS protocol error envelopes (error-code
250/// sanitization and the awsQueryCompatible header), used by the JSON and CBOR
251/// codecs.
252pub mod error_envelope {
253    pub use crate::schema::error_envelope::*;
254}
255
256/// HTTP client-protocol building blocks: HTTP bindings and RPC payloads.
257pub mod http_protocol {
258    pub use crate::schema::http_protocol::*;
259}
260
261/// Runtime type and error registries for resolving shapes by `ShapeId`.
262pub mod registry {
263    pub use crate::schema::registry::*;
264}
265
266use schema::traits as trait_types;
267
268/// A member's position in its parent's member array, stored in one word.
269///
270/// `Option<usize>` would be the obvious type, but it is 16 bytes on a 64-bit target:
271/// the niche-free `usize` forces a separate discriminant word. Encoding absence as `0`
272/// and a present index as `index + 1` fits the same information in 8 bytes. That matters
273/// because [`Schema`] is instantiated once per shape *and* once per member, so thousands
274/// of times in a generated client, and the reclaimed word funds the response routing word
275/// without growing the struct.
276///
277/// The encoding is contained entirely within this type. [`Schema::member_index`] returns
278/// `Option<usize>` exactly as before, and the [`std::fmt::Debug`] impl below prints the decoded
279/// `Option` so `Schema`'s derived `Debug` output is unchanged.
280#[derive(Clone, Copy, PartialEq, Eq)]
281struct MemberIndex(usize);
282
283impl MemberIndex {
284    /// Not a member schema.
285    const NONE: Self = Self(0);
286
287    /// Encodes `index`.
288    ///
289    /// # Panics
290    ///
291    /// If `index == usize::MAX`, which would overflow the encoding. Generated schemas are
292    /// built in const context, so an out-of-range index is a compile-time error rather
293    /// than a runtime one. No real model comes close: the largest structure observed
294    /// across AWS models has 42 members.
295    const fn new(index: usize) -> Self {
296        assert!(
297            index != usize::MAX,
298            "member index must be less than usize::MAX"
299        );
300        Self(index + 1)
301    }
302
303    /// Decodes back to `Option<usize>`.
304    const fn get(self) -> Option<usize> {
305        match self.0 {
306            0 => None,
307            encoded => Some(encoded - 1),
308        }
309    }
310}
311
312impl std::fmt::Debug for MemberIndex {
313    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314        // Print the decoded value so the encoding never leaks into diagnostics and
315        // `Schema`'s derived `Debug` keeps its previous output.
316        self.get().fmt(f)
317    }
318}
319
320/// Bit layout of the response routing word derived by [`Schema::new_struct`].
321///
322/// The word answers, in one load, the two questions a protocol asks of a response
323/// structure: which members are bound to somewhere other than the body, and is there
324/// anything left for the body codec to read. Deriving it in const evaluation keeps the
325/// runtime off the O(all members) trait-checking path the previous implementation used.
326mod response_routing {
327    /// Member positions the mask can represent: bits `0..=61`.
328    pub(crate) const MASK_BITS: usize = 62;
329
330    /// Covers the representable member positions.
331    pub(crate) const MASK: u64 = (1u64 << MASK_BITS) - 1;
332
333    /// Bit 62: at least one member has no response binding, so on a response it belongs
334    /// to the protocol document body and the body codec must be invoked.
335    pub(crate) const HAS_BODY_MEMBERS: u64 = 1u64 << 62;
336
337    /// Bit 63: a response-bound member sits at a position the mask cannot represent.
338    /// Consumers MUST ignore [`MASK`] entirely when this is set and scan the member slice
339    /// instead; the mask is then incomplete, not merely approximate.
340    pub(crate) const NEEDS_SCAN: u64 = 1u64 << 63;
341}
342
343/// A Smithy schema — a lightweight runtime representation of a Smithy shape.
344///
345/// Contains the shape's ID, type, traits relevant to serialization, and
346/// references to member schemas (for aggregate types).
347///
348/// Schemas are constructed at compile time (via `const`) for generated code
349/// and prelude types. The Smithy type system is closed, so no extensibility
350/// via trait objects is needed.
351#[derive(Debug)]
352pub struct Schema<'a> {
353    id: ShapeId<'a>,
354    shape_type: ShapeType,
355    /// Member name if this is a member schema.
356    member_name: Option<&'a str>,
357    /// Member index for position-based lookup in generated code, encoded so it costs
358    /// one word instead of two. See [`MemberIndex`].
359    member_index: MemberIndex,
360    /// Response binding routing, derived in const evaluation by
361    /// [`new_struct`](Schema::new_struct). See [`response_routing`] for the bit layout
362    /// and the accessors below for how to read it.
363    ///
364    /// Zero for every non-struct schema, which reads as "no bound members, no body
365    /// members, mask exact". That is the conservative default: it fails the
366    /// body-only fast path's `HAS_BODY_MEMBERS` requirement, so such a schema routes
367    /// through the full composite rather than silently skipping work.
368    response_routing: u64,
369    /// Shape-type-specific member data.
370    members: SchemaMembers<'a>,
371
372    /// The pre-synthesis shape name for synthetic operation input/output
373    /// shapes.
374    ///
375    /// Smithy's `OperationNormalizer` rewrites every operation's input/output
376    /// to a synthetic shape named `OperationNameInput` / `OperationNameOutput`;
377    /// this field surfaces the name component of
378    /// `SyntheticInputTrait::originalId` / `SyntheticOutputTrait::originalId`,
379    /// which preserves the user-authored shape name. `None` for non-synthetic
380    /// shapes and for member schemas.
381    ///
382    /// Currently consumed by the REST XML codec to determine the body root
383    /// element name when no `@xmlName` overrides it. Distinct from `xml_name`,
384    /// which carries an `@xmlName` trait value. Other consumers (logging,
385    /// future protocols) may also read this field.
386    original_name: Option<&'a str>,
387
388    // -- Known serde trait fields (const-constructable) --
389    // IMPORTANT: These fields and their `with_*` setters must stay in sync with
390    // `knownTraitSetter` in `SchemaGenerator.kt`. If a new known trait is added
391    // here, a corresponding entry must be added in the codegen.
392    sensitive: Option<trait_types::SensitiveTrait>,
393    json_name: Option<trait_types::JsonNameTrait<'a>>,
394    timestamp_format: Option<trait_types::TimestampFormatTrait>,
395    xml_name: Option<trait_types::XmlNameTrait<'a>>,
396    xml_attribute: Option<trait_types::XmlAttributeTrait>,
397    xml_flattened: Option<trait_types::XmlFlattenedTrait>,
398    /// Marks an operation output struct whose XML wire format omits the
399    /// outer wrapper element (set by codegen for operations carrying the
400    /// AWS-customization `S3UnwrappedXmlOutputTrait`). Honored by the XML
401    /// codec; ignored by other codecs (so the schema remains protocol-neutral
402    /// — runtime protocol swap is unaffected).
403    xml_unwrapped_output: bool,
404    /// `true` (the default, conservative) means this struct has at least one
405    /// member that serializes to the request/response body — i.e., a member
406    /// without any HTTP binding trait, OR a member with `@httpPayload`
407    /// targeting a struct/union (which provides body framing through the codec).
408    ///
409    /// `false` (set by codegen via [`with_no_body_members`](Schema::with_no_body_members))
410    /// means every member is HTTP-bound (header / query / label / prefix-headers
411    /// / query-params, or scalar `@httpPayload` whose bytes go into the request
412    /// body raw). The HTTP binding protocol uses this to skip body codec
413    /// invocation entirely on the request side: no XML/JSON wrapper element
414    /// is opened, no `serialize_members` re-entry through the codec proxy
415    /// happens, and the body bytes are never collected (they'd be discarded
416    /// anyway). Saves ~15-20% on header-heavy SER cases like S3 PutObject.
417    has_body_members: bool,
418    /// Records whether an `@httpPayload` member supplies its own body framing,
419    /// so the HTTP binding protocol does not have to scan members to find out.
420    ///
421    /// Defaults to [`PayloadHint::Unknown`], which means "not recorded", the
422    /// runtime then derives the answer itself. See [`PayloadHint`] for why that
423    /// fallback is permanent rather than transitional.
424    payload_hint: PayloadHint,
425    xml_namespace: Option<trait_types::XmlNamespaceTrait<'a>>,
426    /// Deliberately pinned to `'static` while the other seven trait values
427    /// carry `'a`. `Headers::insert` takes `impl AsHeaderComponent`, whose
428    /// zero-allocation impl is for `&'static str`
429    /// (`aws-smithy-runtime-api`'s `MaybeStatic = Cow<'static, str>`), and the
430    /// HTTP binder receives member schemas through `ShapeSerializer` methods
431    /// whose `&Schema<'_>` lifetime is unrelated to the binder's.
432    ///
433    /// The field carries `'a` like every other trait, and the `'static`
434    /// requirement lives on the *constructor* instead:
435    /// [`with_http_header`](Schema::with_http_header) takes `&'static str`, and
436    /// `HttpHeaderTrait::value_static` recovers it for the binder. Keeping the
437    /// restriction at the constructor rather than in the type means relaxing it
438    /// later is additive rather than a breaking signature change, and it avoids
439    /// pinning `ClientProtocol::serialize_request` — which would force every
440    /// protocol to take a `'static` schema.
441    ///
442    /// So: runtime-materialized schemas currently set header names from
443    /// interned/leaked strings, while every other wire-format trait already
444    /// accepts an arbitrary lifetime.
445    http_header: Option<trait_types::HttpHeaderTrait<'a>>,
446    http_label: Option<trait_types::HttpLabelTrait>,
447    http_payload: Option<trait_types::HttpPayloadTrait>,
448    http_prefix_headers: Option<trait_types::HttpPrefixHeadersTrait<'a>>,
449    http_query: Option<trait_types::HttpQueryTrait<'a>>,
450    http_query_params: Option<trait_types::HttpQueryParamsTrait>,
451    http_response_code: Option<trait_types::HttpResponseCodeTrait>,
452    /// The `@http` trait — an operation-level trait included on the input schema
453    /// for convenience so the protocol serializer can construct the request URI.
454    http: Option<trait_types::HttpTrait<'a>>,
455    streaming: Option<trait_types::StreamingTrait>,
456    event_header: Option<trait_types::EventHeaderTrait>,
457    event_payload: Option<trait_types::EventPayloadTrait>,
458    host_label: Option<trait_types::HostLabelTrait>,
459    media_type: Option<trait_types::MediaTypeTrait<'a>>,
460
461    /// Fallback for unknown/custom traits. `None` in const contexts (no allocation).
462    traits: Option<&'a std::sync::LazyLock<TraitMap>>,
463
464    /// Cached derived data. See [`Schema::extension`].
465    extensions: schema::extension::ExtensionStorage,
466}
467
468/// Shape-type-specific member references.
469#[derive(Debug)]
470enum SchemaMembers<'a> {
471    /// No members (simple types).
472    None,
473    /// Structure or union members.
474    Struct { members: &'a [&'a Schema<'a>] },
475    /// List member schema.
476    List { member: &'a Schema<'a> },
477    /// Map key and value schemas.
478    Map {
479        key: &'a Schema<'a>,
480        value: &'a Schema<'a>,
481    },
482}
483
484// ---------- Variance assertions ----------
485//
486// `Schema<'a>`, `ShapeId<'a>`, and the eight trait-value wrappers must remain
487// **covariant** in `'a`. Covariance
488// is what lets `&'static Schema<'static>` (the codegen-emitted form) coerce
489// implicitly into `&Schema<'_>` at call sites — without it, every call would
490// need an explicit lifetime annotation.
491//
492// Today all of them are covariant by construction: every field is of the form
493// `&'a T` or contains another covariant `'a`-parameterized type. There is no
494// `&'a mut T`, no `fn(&'a T)`, and no interior mutability tying `'a` invariantly.
495//
496// These assertion functions make the covariance requirement *load-bearing on
497// the build*. If a future field changes that property — e.g. someone adds a
498// `RefCell` holding a `&'a` reference, an `fn(&'a Self)` field, or a `*mut T`
499// with `'a` — the function bodies will fail to type-check and the build breaks
500// before any downstream code is affected.
501//
502// Compile-time only; zero runtime cost.
503
504#[allow(dead_code)]
505fn _assert_schema_covariant<'a, 'b: 'a>(s: &'b Schema<'b>) -> &'a Schema<'a> {
506    s
507}
508
509#[allow(dead_code)]
510fn _assert_shape_id_covariant<'a, 'b: 'a>(id: ShapeId<'b>) -> ShapeId<'a> {
511    id
512}
513
514// The eight trait-value wrappers carry `'a` too, and the same reasoning
515// applies: a codegen-emitted `JsonNameTrait<'static>` has to be usable
516// wherever a `JsonNameTrait<'_>` is expected. `HttpHeaderTrait` is included
517// even though its only public constructor takes `&'static str` — its private
518// `HeaderName<'a>` enum has a `Borrowed(&'a str)` arm, and this assertion is
519// what keeps that arm covariant if it ever becomes constructible.
520macro_rules! assert_wrapper_covariant {
521    ($fn_name:ident, $wrapper:ident) => {
522        #[allow(dead_code)]
523        fn $fn_name<'a, 'b: 'a>(t: trait_types::$wrapper<'b>) -> trait_types::$wrapper<'a> {
524            t
525        }
526    };
527}
528
529assert_wrapper_covariant!(_assert_json_name_covariant, JsonNameTrait);
530assert_wrapper_covariant!(_assert_xml_name_covariant, XmlNameTrait);
531assert_wrapper_covariant!(_assert_media_type_covariant, MediaTypeTrait);
532assert_wrapper_covariant!(_assert_http_query_covariant, HttpQueryTrait);
533assert_wrapper_covariant!(
534    _assert_http_prefix_headers_covariant,
535    HttpPrefixHeadersTrait
536);
537assert_wrapper_covariant!(_assert_http_header_covariant, HttpHeaderTrait);
538assert_wrapper_covariant!(_assert_xml_namespace_covariant, XmlNamespaceTrait);
539assert_wrapper_covariant!(_assert_http_covariant, HttpTrait);
540
541// Generated schemas are `static`s shared across threads, so `Schema` must stay
542// `Send + Sync`. Extension storage preserves both because cached values are
543// required to be `Send + Sync` themselves.
544const _: () = {
545    const fn assert_send_sync<T: Send + Sync>() {}
546    assert_send_sync::<Schema<'static>>();
547};
548
549impl<'a> Schema<'a> {
550    /// Default values for all fields. Constructors start from this value and assign the
551    /// fields they set; see the `INVARIANT` comment on the trait setters for why they
552    /// do not use it with struct-update syntax.
553    ///
554    /// Implemented as a `const fn` rather than a `const` so it can be
555    /// parameterized over the schema lifetime — `const` items cannot
556    /// have lifetime parameters in stable Rust.
557    const fn empty_traits() -> Schema<'a> {
558        Schema {
559            id: ShapeId::<'a>::from_parts("", "", ""),
560            shape_type: ShapeType::Boolean,
561            member_name: None,
562            member_index: MemberIndex::NONE,
563            response_routing: 0,
564            members: SchemaMembers::None,
565            original_name: None,
566            sensitive: None,
567            json_name: None,
568            timestamp_format: None,
569            xml_name: None,
570            xml_attribute: None,
571            xml_flattened: None,
572            xml_unwrapped_output: false,
573            has_body_members: true,
574            payload_hint: PayloadHint::Unknown,
575            xml_namespace: None,
576            http_header: None,
577            http_label: None,
578            http_payload: None,
579            http_prefix_headers: None,
580            http_query: None,
581            http_query_params: None,
582            http_response_code: None,
583            http: None,
584            streaming: None,
585            event_header: None,
586            event_payload: None,
587            host_label: None,
588            media_type: None,
589            traits: None,
590            extensions: schema::extension::ExtensionStorage::new(),
591        }
592    }
593
594    /// Creates a schema for a simple type (no members).
595    pub const fn new(id: ShapeId<'a>, shape_type: ShapeType) -> Self {
596        let mut schema = Self::empty_traits();
597        schema.id = id;
598        schema.shape_type = shape_type;
599        schema
600    }
601
602    /// Creates a schema for a structure or union type.
603    ///
604    /// `id` and `members` are borrowed for `'a`, so both must outlive the
605    /// returned schema. For codegen-emitted schemas this is always `'static`
606    /// to `'static`. Hand-written runtime construction can use any lifetime
607    /// — typically the surrounding function body's:
608    ///
609    /// ```
610    /// use aws_smithy_schema::{Schema, ShapeId, ShapeType};
611    ///
612    /// let id = ShapeId::from_parts("ns#Foo", "ns", "Foo");
613    /// let member_x = Schema::new_member(
614    ///     ShapeId::from_parts("smithy.api#String", "smithy.api", "String"),
615    ///     ShapeType::String,
616    ///     "x",
617    ///     0,
618    /// );
619    /// let members: [&Schema<'_>; 1] = [&member_x];
620    /// let schema = Schema::new_struct(id, ShapeType::Structure, &members);
621    ///
622    /// assert_eq!(schema.shape_id().as_str(), "ns#Foo");
623    /// assert_eq!(schema.shape_type(), ShapeType::Structure);
624    /// ```
625    ///
626    /// `id`, `member_x`, `members`, and `schema` all share the surrounding
627    /// scope's lifetime; the borrow checker enforces that `members[0]`
628    /// outlives `schema`.
629    pub const fn new_struct(
630        id: ShapeId<'a>,
631        shape_type: ShapeType,
632        members: &'a [&'a Schema<'a>],
633    ) -> Self {
634        let mut schema = Self::empty_traits();
635        schema.id = id;
636        schema.shape_type = shape_type;
637        schema.response_routing = Self::derive_response_routing(members);
638        schema.members = SchemaMembers::Struct { members };
639        schema
640    }
641
642    /// Derives the response routing word from a structure's member slice.
643    ///
644    /// Runs in const evaluation, so generated schemas pay nothing at runtime and an
645    /// out-of-order member array is a compile error rather than a silently wrong mask.
646    ///
647    /// A member is *response-bound* when it carries `@httpHeader`,
648    /// `@httpPrefixHeaders`, `@httpResponseCode`, or `@httpPayload`. Every other member
649    /// — including one carrying only a request-side binding such as `@httpQuery` or
650    /// `@httpLabel` — belongs to the document body on a response, and so sets
651    /// [`response_routing::HAS_BODY_MEMBERS`] rather than a mask bit.
652    const fn derive_response_routing(members: &[&Schema<'a>]) -> u64 {
653        let mut word: u64 = 0;
654        let mut i = 0;
655        while i < members.len() {
656            let member = members[i];
657
658            // The mask encodes a member's identity as its position in this slice, so the
659            // two must agree. `member_schema_by_index` already depends on the same
660            // ordering; asserting here turns a silent mismatch into a build failure at
661            // the point the schema is declared.
662            let index = match member.member_index.get() {
663                Some(index) => index,
664                None => panic!("a struct member schema must carry a member index"),
665            };
666            assert!(
667                index == i,
668                "struct member schemas must be listed in member-index order, because the \
669                 response routing mask uses a member's array position as its index"
670            );
671
672            if member.has_http_response_binding() {
673                if i < response_routing::MASK_BITS {
674                    word |= 1u64 << i;
675                } else {
676                    // Out of mask range: the mask can no longer describe every bound
677                    // member, so consumers must scan. Earlier bits stay set but must be
678                    // ignored once this is flagged.
679                    word |= response_routing::NEEDS_SCAN;
680                }
681            } else {
682                word |= response_routing::HAS_BODY_MEMBERS;
683            }
684            i += 1;
685        }
686        word
687    }
688
689    /// Creates a schema for a list type.
690    pub const fn new_list(id: ShapeId<'a>, member: &'a Schema<'a>) -> Self {
691        let mut schema = Self::empty_traits();
692        schema.id = id;
693        schema.shape_type = ShapeType::List;
694        schema.members = SchemaMembers::List { member };
695        schema
696    }
697
698    /// Creates a schema for a map type.
699    pub const fn new_map(id: ShapeId<'a>, key: &'a Schema<'a>, value: &'a Schema<'a>) -> Self {
700        let mut schema = Self::empty_traits();
701        schema.id = id;
702        schema.shape_type = ShapeType::Map;
703        schema.members = SchemaMembers::Map { key, value };
704        schema
705    }
706
707    /// Creates a member schema wrapping a target schema.
708    pub const fn new_member(
709        id: ShapeId<'a>,
710        shape_type: ShapeType,
711        member_name: &'a str,
712        member_index: usize,
713    ) -> Self {
714        let mut schema = Self::empty_traits();
715        schema.id = id;
716        schema.shape_type = shape_type;
717        schema.member_name = Some(member_name);
718        schema.member_index = MemberIndex::new(member_index);
719        schema
720    }
721
722    /// Returns the Shape ID of this schema.
723    ///
724    /// The returned reference's outer lifetime is the receiver's; the
725    /// inner `ShapeId<'a>` carries the data's lifetime, which is
726    /// `'static` for codegen-emitted schemas (preserving the
727    /// `&'static str` accessor guarantees that downstream
728    /// optimizations rely on).
729    pub fn shape_id(&self) -> &ShapeId<'a> {
730        &self.id
731    }
732
733    /// Returns the shape type.
734    pub fn shape_type(&self) -> ShapeType {
735        self.shape_type
736    }
737
738    /// Returns the fallback trait map for unknown/custom traits.
739    pub fn traits(&self) -> Option<&TraitMap> {
740        self.traits.map(|lazy| &**lazy)
741    }
742
743    // -- Known trait accessors --
744
745    /// Returns the `@sensitive` trait if present.
746    pub fn sensitive(&self) -> Option<&trait_types::SensitiveTrait> {
747        self.sensitive.as_ref()
748    }
749
750    /// Returns the `@jsonName` value if present.
751    pub fn json_name(&self) -> Option<&trait_types::JsonNameTrait<'a>> {
752        self.json_name.as_ref()
753    }
754
755    /// Returns the `@timestampFormat` if present.
756    pub fn timestamp_format(&self) -> Option<&trait_types::TimestampFormatTrait> {
757        self.timestamp_format.as_ref()
758    }
759
760    /// Returns the `@xmlName` value if present.
761    pub fn xml_name(&self) -> Option<&trait_types::XmlNameTrait<'a>> {
762        self.xml_name.as_ref()
763    }
764
765    /// Returns the `@xmlNamespace` value if present.
766    pub fn xml_namespace(&self) -> Option<&trait_types::XmlNamespaceTrait<'a>> {
767        self.xml_namespace.as_ref()
768    }
769
770    /// Returns `true` if this member has the `@xmlAttribute` trait.
771    pub fn xml_attribute(&self) -> bool {
772        self.xml_attribute.is_some()
773    }
774
775    /// Returns `true` if this member has the `@xmlFlattened` trait.
776    pub fn xml_flattened(&self) -> bool {
777        self.xml_flattened.is_some()
778    }
779
780    /// Returns `true` if this struct's XML wire format omits the outer
781    /// wrapper element. See field doc for details.
782    pub fn xml_unwrapped_output(&self) -> bool {
783        self.xml_unwrapped_output
784    }
785
786    /// Returns `true` if this shape has the `@streaming` trait.
787    ///
788    /// In Smithy the trait is applied to a blob or union *shape*, never to a member, so
789    /// codegen propagates it from a member's target into the combined member schema. That
790    /// makes the distinction available here without model access: a protocol can tell a
791    /// streaming payload member — whose live body or event receiver is owned elsewhere —
792    /// from a buffered one it should read itself.
793    pub fn streaming(&self) -> bool {
794        self.streaming.is_some()
795    }
796
797    /// Returns `true` if this struct has at least one member that serializes
798    /// to the request/response body, `false` if every member is HTTP-bound.
799    pub fn has_body_members(&self) -> bool {
800        self.has_body_members
801    }
802
803    /// Returns the recorded [`PayloadHint`], or [`PayloadHint::Unknown`] if none
804    /// was recorded.
805    ///
806    /// Callers MUST handle `Unknown` by deriving the answer themselves; it is
807    /// the default for every hand-constructed schema and for schemas generated
808    /// before codegen began recording it.
809    pub fn payload_hint(&self) -> PayloadHint {
810        self.payload_hint
811    }
812
813    /// Returns `true` if this member schema has any HTTP response binding trait
814    /// (`@httpHeader`, `@httpResponseCode`, `@httpPrefixHeaders`, or `@httpPayload`).
815    pub const fn has_http_response_binding(&self) -> bool {
816        self.http_header.is_some()
817            || self.http_response_code.is_some()
818            || self.http_prefix_headers.is_some()
819            || self.http_payload.is_some()
820    }
821
822    /// Bit set of member positions that carry a response binding.
823    ///
824    /// Bit `i` corresponds to `members()[i]`, so a set bit can be turned into a member
825    /// schema with [`member_schema_by_index`](Schema::member_schema_by_index) in O(1) and
826    /// the whole set walked in O(bound members) rather than O(all members):
827    ///
828    /// ```ignore
829    /// let mut mask = schema.response_binding_mask();
830    /// while mask != 0 {
831    ///     let index = mask.trailing_zeros() as usize;
832    ///     mask &= mask - 1;
833    ///     let member = schema.member_schema_by_index(index).expect("mask derived from members");
834    /// }
835    /// ```
836    ///
837    /// Only meaningful when [`response_bindings_need_scan`](Schema::response_bindings_need_scan)
838    /// is `false`; otherwise it is incomplete and the member slice must be scanned.
839    pub(crate) const fn response_binding_mask(&self) -> u64 {
840        self.response_routing & response_routing::MASK
841    }
842
843    /// Returns `true` if at least one member belongs to the document body on a response,
844    /// meaning the body codec has something to read.
845    ///
846    /// `false` means every member is bound elsewhere in the message, so the body codec
847    /// can be skipped entirely.
848    pub(crate) const fn has_response_body_members(&self) -> bool {
849        self.response_routing & response_routing::HAS_BODY_MEMBERS != 0
850    }
851
852    /// Returns `true` if the binding mask cannot describe every response-bound member, so
853    /// callers must scan [`members`](Schema::members) and test
854    /// [`has_http_response_binding`](Schema::has_http_response_binding) instead.
855    ///
856    /// Only reachable for a structure with a response-bound member at position 62 or
857    /// later. The largest structure observed across AWS models has 42 members, so this is
858    /// a correctness fallback rather than a path real models take.
859    pub(crate) const fn response_bindings_need_scan(&self) -> bool {
860        self.response_routing & response_routing::NEEDS_SCAN != 0
861    }
862
863    /// Returns the `@httpHeader` value if present.
864    ///
865    /// Use [`HttpHeaderTrait::value_static`](trait_types::HttpHeaderTrait::value_static)
866    /// rather than `value()` when inserting into `Headers`, which needs a
867    /// `'static` name; see the field's comment for why.
868    pub fn http_header(&self) -> Option<&trait_types::HttpHeaderTrait<'a>> {
869        self.http_header.as_ref()
870    }
871
872    /// Returns the `@httpQuery` value if present.
873    pub fn http_query(&self) -> Option<&trait_types::HttpQueryTrait<'a>> {
874        self.http_query.as_ref()
875    }
876
877    /// Returns the `@httpLabel` trait if present.
878    pub fn http_label(&self) -> Option<&trait_types::HttpLabelTrait> {
879        self.http_label.as_ref()
880    }
881
882    /// Returns the `@httpPayload` trait if present.
883    pub fn http_payload(&self) -> Option<&trait_types::HttpPayloadTrait> {
884        self.http_payload.as_ref()
885    }
886
887    /// Returns the `@httpPrefixHeaders` value if present.
888    pub fn http_prefix_headers(&self) -> Option<&trait_types::HttpPrefixHeadersTrait<'a>> {
889        self.http_prefix_headers.as_ref()
890    }
891
892    /// Returns the `@mediaType` trait if present.
893    pub fn media_type(&self) -> Option<&trait_types::MediaTypeTrait<'a>> {
894        self.media_type.as_ref()
895    }
896
897    /// Returns the `@httpQueryParams` trait if present.
898    pub fn http_query_params(&self) -> Option<&trait_types::HttpQueryParamsTrait> {
899        self.http_query_params.as_ref()
900    }
901
902    /// Returns the `@httpResponseCode` trait if present.
903    pub fn http_response_code(&self) -> Option<&trait_types::HttpResponseCodeTrait> {
904        self.http_response_code.as_ref()
905    }
906
907    /// Returns the `@http` trait if present.
908    ///
909    /// This is an operation-level trait included on the input schema for
910    /// convenience so the protocol serializer can construct the request URI.
911    pub fn http(&self) -> Option<&trait_types::HttpTrait<'a>> {
912        self.http.as_ref()
913    }
914
915    /// Returns the `@eventHeader` trait if present.
916    pub fn event_header(&self) -> Option<&trait_types::EventHeaderTrait> {
917        self.event_header.as_ref()
918    }
919
920    /// Returns the `@eventPayload` trait if present.
921    pub fn event_payload(&self) -> Option<&trait_types::EventPayloadTrait> {
922        self.event_payload.as_ref()
923    }
924
925    // -- Const setters for builder-style construction in generated code --
926
927    /// Sets the original (pre-synthesis) shape name for synthetic operation
928    /// input/output shapes. See [`Schema::original_name`] for semantics.
929    pub const fn with_original_name(mut self, name: &'a str) -> Self {
930        self.original_name = Some(name);
931        self
932    }
933
934    /// Attaches key and value member schemas to a map member schema.
935    /// Used by the XML codec to resolve `<key>` and `<value>` element names.
936    pub const fn with_map_members(mut self, key: &'a Schema<'a>, value: &'a Schema<'a>) -> Self {
937        self.members = SchemaMembers::Map { key, value };
938        self
939    }
940
941    /// Sets the list member schema on a member schema that targets a list.
942    pub const fn with_list_member(mut self, member: &'a Schema<'a>) -> Self {
943        self.members = SchemaMembers::List { member };
944        self
945    }
946
947    /// Sets the `@sensitive` trait.
948    pub const fn with_sensitive(mut self) -> Self {
949        self.sensitive = Some(trait_types::SensitiveTrait);
950        self
951    }
952
953    // -- Trait-wrapper setters --
954    //
955    // The setters below construct typed trait wrappers (`JsonNameTrait`,
956    // `XmlNameTrait`, `HttpTrait`, etc.). All of them take `&'a str` — the
957    // schema's data lifetime — so a schema materialized at runtime can carry
958    // wire-format traits borrowed from storage the caller owns, such as an
959    // arena holding the parsed model text. Codegen-emitted schemas pass string
960    // literals, so for them `'a` is `'static` and nothing changes.
961    //
962    // `with_http_header` is the one exception and takes `&'static str`; see its
963    // own doc comment and the `http_header` field comment for why, and note the
964    // restriction is at the constructor rather than in the type, so relaxing it
965    // later is additive.
966    //
967    // INVARIANT — do not introduce a trait field that needs drop.
968    //
969    // Every `with_*` setter is a `const fn`, and assigning to a field drops the
970    // field's previous value. A `const fn` body cannot run destructors, so a
971    // trait field with drop glue makes *every* setter for it illegal in const
972    // context (E0493). That matters because generated schemas are
973    // const-initialized statics:
974    //
975    //     static FOO_SCHEMA: Schema<'static> =
976    //         Schema::new_member(..).with_http_header("x-amz-...");
977    //
978    // and a const initializer cannot allocate. So trait values are `&'a str`
979    // and must stay borrow-only: `Cow<'static, str>`, `String`, or any enum
980    // with an owning arm are all ruled out, however convenient they look. A
981    // borrowed-only enum is fine — `HttpHeaderTrait` uses one — because
982    // references have no drop glue.
983    //
984    // `Schema` as a whole does have drop glue, from its extension storage. That
985    // is why the `new_*` constructors start from `Self::empty_traits()` and
986    // assign fields one at a time: struct-update syntax
987    // (`..Self::empty_traits()`) would drop the unused remainder of the base
988    // value, extension storage included, and fail with E0493. Setters are
989    // unaffected because they only overwrite fields that have no drop glue and
990    // then move `self` out whole.
991    //
992    // Keeping the fields borrow-only also preserves the niche optimization
993    // that makes `Option<JsonNameTrait<'a>>` 16 bytes rather than 24, which at
994    // SDK scale is thousands of schemas per crate.
995
996    /// Sets the `@jsonName` trait.
997    pub const fn with_json_name(mut self, value: &'a str) -> Self {
998        self.json_name = Some(trait_types::JsonNameTrait::new(value));
999        self
1000    }
1001
1002    /// Sets the `@timestampFormat` trait.
1003    pub const fn with_timestamp_format(mut self, format: trait_types::TimestampFormat) -> Self {
1004        self.timestamp_format = Some(trait_types::TimestampFormatTrait::new(format));
1005        self
1006    }
1007
1008    /// Sets the `@xmlName` trait.
1009    pub const fn with_xml_name(mut self, value: &'a str) -> Self {
1010        self.xml_name = Some(trait_types::XmlNameTrait::new(value));
1011        self
1012    }
1013
1014    /// Sets the `@xmlAttribute` trait.
1015    pub const fn with_xml_attribute(mut self) -> Self {
1016        self.xml_attribute = Some(trait_types::XmlAttributeTrait);
1017        self
1018    }
1019
1020    /// Sets the `@xmlFlattened` trait.
1021    pub const fn with_xml_flattened(mut self) -> Self {
1022        self.xml_flattened = Some(trait_types::XmlFlattenedTrait);
1023        self
1024    }
1025
1026    /// Marks the struct as an unwrapped XML output. See field doc for details.
1027    pub const fn with_xml_unwrapped_output(mut self) -> Self {
1028        self.xml_unwrapped_output = true;
1029        self
1030    }
1031
1032    /// Marks this struct as having no body members — every member is HTTP-bound.
1033    /// See [`has_body_members`](Schema::has_body_members) for what this enables.
1034    pub const fn with_no_body_members(mut self) -> Self {
1035        self.has_body_members = false;
1036        self
1037    }
1038
1039    /// Records whether an `@httpPayload` member supplies its own body framing.
1040    ///
1041    /// Setting this lets the HTTP binding protocol skip a per-request scan of
1042    /// this schema's members. It is an optimization only: leaving it at
1043    /// [`PayloadHint::Unknown`] (the default) produces identical output. See
1044    /// [`PayloadHint`] for why.
1045    pub const fn with_payload_hint(mut self, hint: PayloadHint) -> Self {
1046        self.payload_hint = hint;
1047        self
1048    }
1049
1050    /// Sets the `@httpHeader` trait.
1051    ///
1052    /// Takes `&'static str`, unlike the other wire-format setters, so the
1053    /// binder's `Headers::insert` fast path stays allocation-free. A schema
1054    /// built at runtime must supply an interned or leaked header name.
1055    ///
1056    /// Relaxing this is additive: a `with_http_header_borrowed(&'a str)`
1057    /// companion can be added without changing this signature or the getter's.
1058    pub const fn with_http_header(mut self, value: &'static str) -> Self {
1059        self.http_header = Some(trait_types::HttpHeaderTrait::new(value));
1060        self
1061    }
1062
1063    /// Sets the `@httpLabel` trait.
1064    pub const fn with_http_label(mut self) -> Self {
1065        self.http_label = Some(trait_types::HttpLabelTrait);
1066        self
1067    }
1068
1069    /// Sets the `@httpPayload` trait.
1070    pub const fn with_http_payload(mut self) -> Self {
1071        self.http_payload = Some(trait_types::HttpPayloadTrait);
1072        self
1073    }
1074
1075    /// Sets the `@httpPrefixHeaders` trait.
1076    pub const fn with_http_prefix_headers(mut self, value: &'a str) -> Self {
1077        self.http_prefix_headers = Some(trait_types::HttpPrefixHeadersTrait::new(value));
1078        self
1079    }
1080
1081    /// Sets the `@httpQuery` trait.
1082    pub const fn with_http_query(mut self, value: &'a str) -> Self {
1083        self.http_query = Some(trait_types::HttpQueryTrait::new(value));
1084        self
1085    }
1086
1087    /// Sets the `@httpQueryParams` trait.
1088    pub const fn with_http_query_params(mut self) -> Self {
1089        self.http_query_params = Some(trait_types::HttpQueryParamsTrait);
1090        self
1091    }
1092
1093    /// Sets the `@httpResponseCode` trait.
1094    pub const fn with_http_response_code(mut self) -> Self {
1095        self.http_response_code = Some(trait_types::HttpResponseCodeTrait);
1096        self
1097    }
1098
1099    /// Sets the `@http` trait (operation-level, included on input schema for convenience).
1100    pub const fn with_http(mut self, http: trait_types::HttpTrait<'a>) -> Self {
1101        self.http = Some(http);
1102        self
1103    }
1104
1105    /// Sets the `@streaming` trait.
1106    pub const fn with_streaming(mut self) -> Self {
1107        self.streaming = Some(trait_types::StreamingTrait);
1108        self
1109    }
1110
1111    /// Sets the `@eventHeader` trait.
1112    pub const fn with_event_header(mut self) -> Self {
1113        self.event_header = Some(trait_types::EventHeaderTrait);
1114        self
1115    }
1116
1117    /// Sets the `@eventPayload` trait.
1118    pub const fn with_event_payload(mut self) -> Self {
1119        self.event_payload = Some(trait_types::EventPayloadTrait);
1120        self
1121    }
1122
1123    /// Sets the `@hostLabel` trait.
1124    pub const fn with_host_label(mut self) -> Self {
1125        self.host_label = Some(trait_types::HostLabelTrait);
1126        self
1127    }
1128
1129    /// Sets the `@mediaType` trait.
1130    pub const fn with_media_type(mut self, value: &'a str) -> Self {
1131        self.media_type = Some(trait_types::MediaTypeTrait::new(value));
1132        self
1133    }
1134
1135    /// Sets the `@xmlNamespace` trait.
1136    ///
1137    /// `uri` is the namespace URI; `prefix` optionally declares the
1138    /// `xmlns:prefix` form. Pass `None` for the default (unprefixed)
1139    /// `xmlns="uri"` declaration.
1140    pub const fn with_xml_namespace(mut self, uri: &'a str, prefix: Option<&'a str>) -> Self {
1141        self.xml_namespace = Some(trait_types::XmlNamespaceTrait::new(uri, prefix));
1142        self
1143    }
1144
1145    /// Sets the fallback trait map for unknown/custom traits.
1146    ///
1147    /// The schema must outlive the `LazyLock` it references. For codegen-emitted
1148    /// schemas this is always `'static` to `'static`. Runtime-built schemas
1149    /// must arrange the lifetimes via standard borrow-check discipline.
1150    pub const fn with_traits(mut self, traits: &'a std::sync::LazyLock<TraitMap>) -> Self {
1151        self.traits = Some(traits);
1152        self
1153    }
1154
1155    /// Returns the value of extension `key` for this schema, computing it on first use.
1156    ///
1157    /// The value is computed by the key's provider the first time this schema is asked
1158    /// for it, then cached for the life of the schema. Concurrent first calls compute it
1159    /// once. See the [`extension`] module.
1160    ///
1161    /// # Panics
1162    ///
1163    /// If called from inside an extension provider for a value that is not yet cached,
1164    /// and if the provider itself panics.
1165    #[inline]
1166    pub fn extension<T: Send + Sync + 'static>(
1167        &self,
1168        key: &'static extension::SchemaExtensionKey<T>,
1169    ) -> &T {
1170        self.extensions.get_or_compute(self, key)
1171    }
1172
1173    /// Returns the member name if this is a member schema.
1174    ///
1175    /// Returns `Option<&'a str>` (the schema's data lifetime, not the
1176    /// receiver's). For `Schema<'static>` (codegen-emitted) this is
1177    /// `Option<&'static str>` and callers can store the name in
1178    /// `Cow::Borrowed` or other `'static`-lifetime contexts without
1179    /// allocating, matching the underlying field type.
1180    pub fn member_name(&self) -> Option<&'a str> {
1181        self.member_name
1182    }
1183
1184    /// Returns the member index for member schemas.
1185    ///
1186    /// This is used internally by generated code for efficient member lookup.
1187    /// Consumer code should not rely on specific position values as they may change.
1188    pub fn member_index(&self) -> Option<usize> {
1189        self.member_index.get()
1190    }
1191
1192    /// Returns the original (pre-synthesis) shape name for synthetic operation
1193    /// input/output shapes.
1194    ///
1195    /// `None` for non-synthetic shapes. See the field documentation for full
1196    /// semantics.
1197    pub fn original_name(&self) -> Option<&str> {
1198        self.original_name
1199    }
1200
1201    /// Returns the member schema by name (for structures and unions).
1202    pub fn member_schema(&self, name: &str) -> Option<&Schema<'_>> {
1203        match &self.members {
1204            SchemaMembers::Struct { members } => members
1205                .iter()
1206                .find(|m| m.member_name == Some(name))
1207                .copied(),
1208            _ => None,
1209        }
1210    }
1211
1212    /// Returns the member name and schema by position index (for structures and unions).
1213    ///
1214    /// This is an optimization for generated code to avoid string lookups.
1215    /// Consumer code should not rely on specific position values as they may change.
1216    pub fn member_schema_by_index(&self, index: usize) -> Option<&Schema<'_>> {
1217        match &self.members {
1218            SchemaMembers::Struct { members } => members.get(index).copied(),
1219            _ => None,
1220        }
1221    }
1222
1223    /// Returns the member schemas (for structures and unions).
1224    pub fn members(&self) -> &[&Schema<'_>] {
1225        match &self.members {
1226            SchemaMembers::Struct { members } => members,
1227            _ => &[],
1228        }
1229    }
1230
1231    /// Returns the member schema for collections (list member or map value).
1232    pub fn member(&self) -> Option<&Schema<'_>> {
1233        match &self.members {
1234            SchemaMembers::List { member } => Some(member),
1235            SchemaMembers::Map { value, .. } => Some(value),
1236            _ => None,
1237        }
1238    }
1239
1240    /// Like [`member`](Self::member) but returns the `'a` (data-lifetime)
1241    /// borrow that codegen actually stores. Use this when the caller must
1242    /// hold a reference to a value/element member schema across nested
1243    /// callbacks without inheriting the parent `&self` borrow's lifetime.
1244    ///
1245    /// For `Schema<'static>` (codegen-emitted) this returns
1246    /// `Option<&'static Schema<'static>>`.
1247    pub fn member_borrowed(&self) -> Option<&'a Schema<'a>> {
1248        match &self.members {
1249            SchemaMembers::List { member } => Some(*member),
1250            SchemaMembers::Map { value, .. } => Some(*value),
1251            _ => None,
1252        }
1253    }
1254
1255    /// Returns the key schema for maps.
1256    pub fn key(&self) -> Option<&Schema<'_>> {
1257        match &self.members {
1258            SchemaMembers::Map { key, .. } => Some(key),
1259            _ => None,
1260        }
1261    }
1262
1263    /// Like [`key`](Self::key) but returns the `'a` (data-lifetime) borrow
1264    /// that codegen stores. See [`member_borrowed`](Self::member_borrowed).
1265    pub fn key_borrowed(&self) -> Option<&'a Schema<'a>> {
1266        match &self.members {
1267            SchemaMembers::Map { key, .. } => Some(*key),
1268            _ => None,
1269        }
1270    }
1271
1272    // -- convenience predicates --
1273
1274    /// Returns true if this is a member schema.
1275    pub fn is_member(&self) -> bool {
1276        self.shape_type.is_member()
1277    }
1278
1279    /// Returns true if this is a structure schema.
1280    pub fn is_structure(&self) -> bool {
1281        self.shape_type == ShapeType::Structure
1282    }
1283
1284    /// Returns true if this is a union schema.
1285    pub fn is_union(&self) -> bool {
1286        self.shape_type == ShapeType::Union
1287    }
1288
1289    /// Returns true if this is a list schema.
1290    pub fn is_list(&self) -> bool {
1291        self.shape_type == ShapeType::List
1292    }
1293
1294    /// Returns true if this is a map schema.
1295    pub fn is_map(&self) -> bool {
1296        self.shape_type == ShapeType::Map
1297    }
1298
1299    /// Returns true if this is a blob schema.
1300    pub fn is_blob(&self) -> bool {
1301        self.shape_type == ShapeType::Blob
1302    }
1303
1304    /// Returns true if this is a string schema.
1305    pub fn is_string(&self) -> bool {
1306        self.shape_type == ShapeType::String
1307    }
1308}
1309
1310#[cfg(test)]
1311mod test {
1312    use crate::{shape_id, MemberIndex, Schema, ShapeId, ShapeType, Trait, TraitMap};
1313
1314    // Simple test trait implementation
1315    #[derive(Debug)]
1316    struct TestTrait {
1317        id: crate::ShapeId<'static>,
1318        #[allow(dead_code)]
1319        value: String,
1320    }
1321
1322    impl Trait for TestTrait {
1323        fn trait_id(&self) -> &crate::ShapeId<'static> {
1324            &self.id
1325        }
1326
1327        fn as_any(&self) -> &dyn std::any::Any {
1328            self
1329        }
1330    }
1331
1332    #[test]
1333    fn test_shape_type_simple() {
1334        assert!(ShapeType::String.is_simple());
1335        assert!(ShapeType::Integer.is_simple());
1336        assert!(ShapeType::Boolean.is_simple());
1337        assert!(!ShapeType::Structure.is_simple());
1338        assert!(!ShapeType::List.is_simple());
1339    }
1340
1341    #[test]
1342    fn test_shape_type_aggregate() {
1343        assert!(ShapeType::Structure.is_aggregate());
1344        assert!(ShapeType::Union.is_aggregate());
1345        assert!(ShapeType::List.is_aggregate());
1346        assert!(ShapeType::Map.is_aggregate());
1347        assert!(!ShapeType::String.is_aggregate());
1348    }
1349
1350    #[test]
1351    fn test_shape_type_member() {
1352        assert!(ShapeType::Member.is_member());
1353        assert!(!ShapeType::String.is_member());
1354        assert!(!ShapeType::Structure.is_member());
1355    }
1356
1357    #[test]
1358    fn test_shape_id_parsing() {
1359        let id = shape_id!("smithy.api", "String");
1360        assert_eq!(id.namespace(), "smithy.api");
1361        assert_eq!(id.shape_name(), "String");
1362        assert_eq!(id.member_name(), None);
1363    }
1364
1365    #[test]
1366    fn test_shape_id_with_member() {
1367        let id = shape_id!("com.example", "MyStruct", "member");
1368        assert_eq!(id.namespace(), "com.example");
1369        assert_eq!(id.shape_name(), "MyStruct");
1370        assert_eq!(id.member_name(), Some("member"));
1371    }
1372
1373    #[test]
1374    fn test_trait_map() {
1375        let mut map = TraitMap::new();
1376        assert!(map.is_empty());
1377        assert_eq!(map.len(), 0);
1378
1379        let trait_id = shape_id!("smithy.api", "required");
1380        let test_trait = Box::new(TestTrait {
1381            id: trait_id.clone(),
1382            value: "test".to_string(),
1383        });
1384
1385        map.insert(test_trait);
1386        assert!(!map.is_empty());
1387        assert_eq!(map.len(), 1);
1388        assert!(map.contains(&trait_id));
1389
1390        let retrieved = map.get(&trait_id);
1391        assert!(retrieved.is_some());
1392    }
1393
1394    /// `TraitMap::get` and `contains` accept any `&ShapeId<'_>`.
1395    /// `get_fqn` / `contains_fqn` accept `&str`. All resolve against the
1396    /// `'static`-keyed table by fully qualified name.
1397    #[test]
1398    fn test_trait_map_cross_lifetime_lookup() {
1399        let mut map = TraitMap::new();
1400        let trait_id = shape_id!("smithy.api", "required");
1401        map.insert(Box::new(TestTrait {
1402            id: trait_id,
1403            value: "test".to_string(),
1404        }));
1405
1406        // `&str` lookup.
1407        assert!(map.contains_fqn("smithy.api#required"));
1408        assert!(map.get_fqn("smithy.api#required").is_some());
1409        assert!(!map.contains_fqn("smithy.api#missing"));
1410
1411        // Runtime-built ShapeId lookup.
1412        let owned_fqn = String::from("smithy.api#required");
1413        let owned_ns = String::from("smithy.api");
1414        let owned_name = String::from("required");
1415        let runtime_id: ShapeId<'_> = ShapeId::from_parts(&owned_fqn, &owned_ns, &owned_name);
1416        assert!(map.contains(&runtime_id));
1417        assert!(map.get(&runtime_id).is_some());
1418    }
1419
1420    #[test]
1421    fn test_schema_predicates() {
1422        let schema = Schema::new(shape_id!("com.example", "MyStruct"), ShapeType::Structure);
1423
1424        assert!(schema.is_structure());
1425        assert!(!schema.is_union());
1426        assert!(!schema.is_list());
1427        assert!(!schema.is_member());
1428    }
1429
1430    #[test]
1431    fn test_schema_basic() {
1432        let schema = Schema::new(shape_id!("smithy.api", "String"), ShapeType::String);
1433
1434        assert_eq!(schema.shape_id().as_str(), "smithy.api#String");
1435        assert_eq!(schema.shape_type(), ShapeType::String);
1436        assert!(schema.traits().is_none());
1437        assert!(schema.member_name().is_none());
1438        assert!(schema.member_schema("test").is_none());
1439        assert!(schema.member_schema_by_index(0).is_none());
1440    }
1441
1442    /// The gap this test exists to close: `shape_id.rs` already covers runtime
1443    /// `ShapeId`s, but nothing covered runtime *trait values*, so a `Schema`
1444    /// could be given the right shape and then not be given a wire format.
1445    ///
1446    /// Every string here is owned by a local `Vec<String>` — an arena the
1447    /// caller controls. Nothing is `'static` and nothing is leaked, which is
1448    /// the property that makes runtime-materialized schemas practical.
1449    #[test]
1450    fn runtime_trait_values_from_an_arena() {
1451        let arena: Vec<String> = vec![
1452            String::from("ns#Foo"),              // 0 fqn
1453            String::from("ns"),                  // 1 namespace
1454            String::from("Foo"),                 // 2 shape name
1455            String::from("fieldName"),           // 3 member name
1456            String::from("WireName"),            // 4 @jsonName
1457            String::from("wire-elem"),           // 5 @xmlName
1458            String::from("application/json"),    // 6 @mediaType
1459            String::from("qparam"),              // 7 @httpQuery
1460            String::from("http://ns.example/x"), // 8 @xmlNamespace uri
1461            String::from("px"),                  // 9 @xmlNamespace prefix
1462        ];
1463
1464        let member: Schema<'_> = Schema::new_member(
1465            ShapeId::from_parts(&arena[0], &arena[1], &arena[2]),
1466            ShapeType::String,
1467            &arena[3],
1468            0,
1469        )
1470        .with_json_name(&arena[4])
1471        .with_xml_name(&arena[5])
1472        .with_media_type(&arena[6])
1473        .with_http_query(&arena[7])
1474        .with_xml_namespace(&arena[8], Some(&arena[9]));
1475
1476        // Read every wire-format trait back off the runtime schema.
1477        assert_eq!(member.json_name().unwrap().value(), "WireName");
1478        assert_eq!(member.xml_name().unwrap().value(), "wire-elem");
1479        assert_eq!(member.media_type().unwrap().value(), "application/json");
1480        assert_eq!(member.http_query().unwrap().value(), "qparam");
1481        let ns = member.xml_namespace().unwrap();
1482        assert_eq!(ns.uri(), "http://ns.example/x");
1483        assert_eq!(ns.prefix(), Some("px"));
1484        assert_eq!(member.member_name(), Some("fieldName"));
1485
1486        // And it composes into a struct schema whose members are readable.
1487        let members = [&member];
1488        let schema = Schema::new_struct(
1489            ShapeId::from_parts(&arena[0], &arena[1], &arena[2]),
1490            ShapeType::Structure,
1491            &members,
1492        );
1493        assert_eq!(
1494            schema
1495                .member_schema("fieldName")
1496                .unwrap()
1497                .json_name()
1498                .unwrap()
1499                .value(),
1500            "WireName"
1501        );
1502    }
1503
1504    /// `@http` carries the method and URI, so without runtime support a
1505    /// dynamically-built operation has no request line at all.
1506    #[test]
1507    fn runtime_http_trait_from_an_arena() {
1508        let method = String::from("PATCH");
1509        let uri = String::from("/things/{id}");
1510
1511        let schema = Schema::new(shape_id!("ns", "Op"), ShapeType::Structure)
1512            .with_http(crate::traits::HttpTrait::new(&method, &uri, Some(204)));
1513
1514        let http = schema.http().unwrap();
1515        assert_eq!(http.method(), "PATCH");
1516        assert_eq!(http.uri(), "/things/{id}");
1517        assert_eq!(http.code(), 204);
1518    }
1519
1520    /// A trait value read off a `Schema<'static>` is still `&'static str`, so
1521    /// the relaxation costs codegen-emitted schemas nothing. This is a
1522    /// compile-time assertion: the annotation is the test.
1523    #[test]
1524    fn static_schema_still_yields_static_trait_values() {
1525        static S: Schema<'static> =
1526            Schema::new(shape_id!("ns", "Foo"), ShapeType::String).with_json_name("WireName");
1527
1528        let value: &'static str = S.json_name().unwrap().value();
1529        assert_eq!(value, "WireName");
1530    }
1531
1532    /// Interning is deduplicated: the same name yields the identical pointer,
1533    /// so a repeated call leaks nothing further. This is the property that
1534    /// makes `intern_header_name` safe where a bare `Box::leak` is not.
1535    #[test]
1536    fn intern_header_name_dedups_by_pointer() {
1537        let from_runtime = String::from("x-dedup-probe");
1538
1539        let a = crate::intern_header_name(&from_runtime);
1540        let b = crate::intern_header_name("x-dedup-probe");
1541        let c = crate::intern_header_name(&String::from("x-dedup-probe"));
1542
1543        assert_eq!(a, "x-dedup-probe");
1544        assert!(std::ptr::eq(a, b), "second intern must reuse the first");
1545        assert!(std::ptr::eq(a, c), "third intern must reuse the first");
1546
1547        // Distinct names are distinct allocations with correct contents.
1548        let other = crate::intern_header_name("x-dedup-other");
1549        assert!(!std::ptr::eq(a, other));
1550        assert_eq!(other, "x-dedup-other");
1551    }
1552
1553    /// The point of interning: a header name that only exists at runtime can be
1554    /// attached to a schema, and the binder still sees a `'static` name, so the
1555    /// zero-allocation insert path is preserved rather than falling back to an
1556    /// owned copy.
1557    #[test]
1558    fn interned_header_name_preserves_the_binder_fast_path() {
1559        let arena: Vec<String> = vec![String::from("memberName"), String::from("x-runtime-hdr")];
1560
1561        let member: Schema<'_> =
1562            Schema::new_member(shape_id!("ns", "Foo"), ShapeType::String, &arena[0], 0)
1563                .with_http_header(crate::intern_header_name(&arena[1]));
1564
1565        // `Some` is what the binder needs; `None` would force an allocation.
1566        assert_eq!(
1567            member.http_header().unwrap().value_static(),
1568            Some("x-runtime-hdr")
1569        );
1570        assert_eq!(member.http_header().unwrap().value(), "x-runtime-hdr");
1571    }
1572
1573    #[test]
1574    fn streaming_accessor_reflects_the_propagated_trait() {
1575        // Codegen propagates `@streaming` from a member's target (the trait's selector is
1576        // `:is(blob, union)`, so a member never carries it directly). This is the accessor
1577        // the protocol uses to tell a streaming payload member from a buffered one.
1578        //
1579        // These are `static`s rather than locals on purpose: that is the position codegen
1580        // emits them in, so this also pins `with_streaming` as const-evaluable when
1581        // chained after another setter.
1582        static BUFFERED: Schema<'static> = Schema::new_member(
1583            shape_id!("com.example", "Output", "body"),
1584            ShapeType::Blob,
1585            "body",
1586            0,
1587        );
1588        assert!(
1589            !BUFFERED.streaming(),
1590            "a member whose target is not streaming must report false"
1591        );
1592
1593        static STREAMING_BLOB: Schema<'static> = Schema::new_member(
1594            shape_id!("com.example", "Output", "body"),
1595            ShapeType::Blob,
1596            "body",
1597            0,
1598        )
1599        .with_streaming();
1600        assert!(STREAMING_BLOB.streaming());
1601
1602        // The union half of the selector: an event-stream member.
1603        static EVENT_STREAM: Schema<'static> = Schema::new_member(
1604            shape_id!("com.example", "Output", "events"),
1605            ShapeType::Union,
1606            "events",
1607            0,
1608        )
1609        .with_streaming();
1610        assert!(EVENT_STREAM.streaming());
1611
1612        // Exactly the chain generated for a streaming payload member. Independent of the
1613        // payload binding, so the payload branch can check both.
1614        static STREAMING_PAYLOAD: Schema<'static> = Schema::new_member(
1615            shape_id!("com.example", "Output", "body"),
1616            ShapeType::Blob,
1617            "body",
1618            0,
1619        )
1620        .with_http_payload()
1621        .with_streaming();
1622        assert!(STREAMING_PAYLOAD.streaming());
1623        assert!(STREAMING_PAYLOAD.http_payload().is_some());
1624
1625        static BUFFERED_PAYLOAD: Schema<'static> = Schema::new_member(
1626            shape_id!("com.example", "Output", "body"),
1627            ShapeType::Blob,
1628            "body",
1629            0,
1630        )
1631        .with_http_payload();
1632        assert!(!BUFFERED_PAYLOAD.streaming());
1633        assert!(BUFFERED_PAYLOAD.http_payload().is_some());
1634    }
1635
1636    /// The size of [`Schema`] is a deliberate budget, not an accident.
1637    ///
1638    /// A generated client instantiates one `Schema` per shape *and* per member, so
1639    /// thousands of them; every added word is multiplied by that count in both the
1640    /// binary and resident memory. The running budget on a 64-bit target:
1641    ///
1642    /// | state | bytes | change |
1643    /// | --- | --- | --- |
1644    /// | baseline | 336 | — |
1645    /// | `member_index` encoded in one word instead of `Option<usize>` | 328 | −8 |
1646    /// | response routing word added | 336 | +8 |
1647    /// | schema extension storage added | 352 | +16 |
1648    ///
1649    /// The routing word was deliberately funded by the `member_index` re-encoding, so
1650    /// response binding routing cost no net memory.
1651    ///
1652    /// If this assertion fails, a field was added or widened. That is not automatically
1653    /// wrong, but it needs to be a conscious decision with SDK-scale source and rlib
1654    /// measurement behind it — so update the number deliberately rather than reflexively.
1655    ///
1656    /// Gated to 64-bit because the layout differs on 32-bit targets.
1657    #[test]
1658    #[cfg(target_pointer_width = "64")]
1659    fn schema_size_stays_within_its_budget() {
1660        assert_eq!(
1661            std::mem::size_of::<Schema<'static>>(),
1662            352,
1663            "Schema changed size; see this test's documentation before updating the number"
1664        );
1665    }
1666
1667    #[test]
1668    fn member_index_encoding_round_trips() {
1669        assert_eq!(MemberIndex::NONE.get(), None);
1670        for index in [0usize, 1, 2, 41, 61, 62, 63, 1000, usize::MAX - 1] {
1671            assert_eq!(
1672                MemberIndex::new(index).get(),
1673                Some(index),
1674                "index {index} did not round-trip"
1675            );
1676        }
1677        // The reason for the encoding: one word instead of two.
1678        assert_eq!(
1679            std::mem::size_of::<MemberIndex>(),
1680            std::mem::size_of::<usize>()
1681        );
1682        assert!(std::mem::size_of::<MemberIndex>() < std::mem::size_of::<Option<usize>>());
1683    }
1684
1685    #[test]
1686    fn member_index_zero_is_distinct_from_absent() {
1687        // The encoding's whole risk is conflating index 0 with "not a member", since both
1688        // are naturally zero.
1689        assert_eq!(MemberIndex::new(0).get(), Some(0));
1690        assert_ne!(MemberIndex::new(0), MemberIndex::NONE);
1691
1692        let member = Schema::new_member(
1693            shape_id!("com.example", "S", "first"),
1694            ShapeType::String,
1695            "first",
1696            0,
1697        );
1698        assert_eq!(
1699            member.member_index(),
1700            Some(0),
1701            "the first member must report index 0, not absent"
1702        );
1703
1704        let non_member = Schema::new(shape_id!("com.example", "S"), ShapeType::Structure);
1705        assert_eq!(non_member.member_index(), None);
1706    }
1707
1708    #[test]
1709    #[should_panic(expected = "member index must be less than usize::MAX")]
1710    fn member_index_rejects_the_unrepresentable_index() {
1711        // `usize::MAX` would overflow `index + 1`. Generated schemas are const-evaluated,
1712        // so in practice this is a compile-time error; this covers the runtime path.
1713        let _ = MemberIndex::new(usize::MAX);
1714    }
1715
1716    #[test]
1717    fn debug_output_shows_the_decoded_member_index() {
1718        // The encoding must not leak into diagnostics. `Schema` derives `Debug`, so this
1719        // pins that the newtype's `Debug` prints the decoded `Option`.
1720        let member = Schema::new_member(
1721            shape_id!("com.example", "S", "third"),
1722            ShapeType::String,
1723            "third",
1724            2,
1725        );
1726        let rendered = format!("{member:?}");
1727        assert!(
1728            rendered.contains("member_index: Some(2)"),
1729            "expected the decoded index in Debug output, got: {rendered}"
1730        );
1731        assert!(
1732            !rendered.contains("member_index: Some(3)") && !rendered.contains("member_index: 3"),
1733            "the raw encoded value leaked into Debug output: {rendered}"
1734        );
1735
1736        let non_member = Schema::new(shape_id!("com.example", "S"), ShapeType::Structure);
1737        assert!(
1738            format!("{non_member:?}").contains("member_index: None"),
1739            "absent index should render as None"
1740        );
1741    }
1742
1743    /// Walks a routing mask the way the composite does, returning visited member indices.
1744    fn mask_indices(schema: &Schema<'_>) -> Vec<usize> {
1745        let mut mask = schema.response_binding_mask();
1746        let mut out = Vec::new();
1747        while mask != 0 {
1748            let index = mask.trailing_zeros() as usize;
1749            mask &= mask - 1;
1750            out.push(index);
1751        }
1752        out
1753    }
1754
1755    #[test]
1756    fn routing_word_marks_only_response_bound_members() {
1757        static HEADER: Schema<'static> =
1758            Schema::new_member(shape_id!("t", "O", "h"), ShapeType::String, "h", 0)
1759                .with_http_header("x-h");
1760        static BODY: Schema<'static> =
1761            Schema::new_member(shape_id!("t", "O", "b"), ShapeType::String, "b", 1);
1762        static STATUS: Schema<'static> =
1763            Schema::new_member(shape_id!("t", "O", "s"), ShapeType::Integer, "s", 2)
1764                .with_http_response_code();
1765        static PREFIX: Schema<'static> =
1766            Schema::new_member(shape_id!("t", "O", "p"), ShapeType::Map, "p", 3)
1767                .with_http_prefix_headers("x-meta-");
1768        static PAYLOAD: Schema<'static> =
1769            Schema::new_member(shape_id!("t", "O", "pay"), ShapeType::Blob, "pay", 4)
1770                .with_http_payload();
1771        static OUTPUT: Schema<'static> = Schema::new_struct(
1772            shape_id!("t", "O"),
1773            ShapeType::Structure,
1774            &[&HEADER, &BODY, &STATUS, &PREFIX, &PAYLOAD],
1775        );
1776
1777        // All four response binding kinds set a bit; the unbound member does not.
1778        assert_eq!(mask_indices(&OUTPUT), vec![0, 2, 3, 4]);
1779        assert!(OUTPUT.has_response_body_members());
1780        assert!(!OUTPUT.response_bindings_need_scan());
1781
1782        // The mask agrees with the per-member predicate it is derived from.
1783        for (i, member) in OUTPUT.members().iter().enumerate() {
1784            assert_eq!(
1785                OUTPUT.response_binding_mask() & (1u64 << i) != 0,
1786                member.has_http_response_binding(),
1787                "mask bit {i} disagrees with has_http_response_binding()"
1788            );
1789        }
1790    }
1791
1792    #[test]
1793    fn request_only_bindings_are_body_members_on_a_response() {
1794        // The distinction the design is explicit about: `@httpQuery` and `@httpLabel` bind
1795        // a member on the request, but on a response it belongs to the document body. So
1796        // they must NOT set a mask bit, and they MUST set the body bit.
1797        static QUERY: Schema<'static> =
1798            Schema::new_member(shape_id!("t", "O", "q"), ShapeType::String, "q", 0)
1799                .with_http_query("q");
1800        static LABEL: Schema<'static> =
1801            Schema::new_member(shape_id!("t", "O", "l"), ShapeType::String, "l", 1)
1802                .with_http_label();
1803        static OUTPUT: Schema<'static> =
1804            Schema::new_struct(shape_id!("t", "O"), ShapeType::Structure, &[&QUERY, &LABEL]);
1805
1806        assert_eq!(
1807            mask_indices(&OUTPUT),
1808            Vec::<usize>::new(),
1809            "request-only bindings must not be treated as response-bound"
1810        );
1811        assert!(
1812            OUTPUT.has_response_body_members(),
1813            "request-only bound members are body members on a response"
1814        );
1815    }
1816
1817    #[test]
1818    fn a_fully_bound_structure_has_no_body_members() {
1819        // This is what lets the composite skip the body codec entirely.
1820        static H1: Schema<'static> =
1821            Schema::new_member(shape_id!("t", "O", "a"), ShapeType::String, "a", 0)
1822                .with_http_header("x-a");
1823        static H2: Schema<'static> =
1824            Schema::new_member(shape_id!("t", "O", "b"), ShapeType::String, "b", 1)
1825                .with_http_header("x-b");
1826        static OUTPUT: Schema<'static> =
1827            Schema::new_struct(shape_id!("t", "O"), ShapeType::Structure, &[&H1, &H2]);
1828
1829        assert_eq!(mask_indices(&OUTPUT), vec![0, 1]);
1830        assert!(!OUTPUT.has_response_body_members());
1831        assert!(!OUTPUT.response_bindings_need_scan());
1832    }
1833
1834    #[test]
1835    fn an_empty_structure_routes_through_the_composite() {
1836        // Zero members: no bindings and no body members. The body bit being clear is what
1837        // keeps an empty output off the bare-codec fast path, so the body codec is never
1838        // invoked on it.
1839        static EMPTY: Schema<'static> =
1840            Schema::new_struct(shape_id!("t", "E"), ShapeType::Structure, &[]);
1841        assert_eq!(EMPTY.response_binding_mask(), 0);
1842        assert!(!EMPTY.has_response_body_members());
1843        assert!(!EMPTY.response_bindings_need_scan());
1844    }
1845
1846    #[test]
1847    fn a_body_only_structure_reports_exactly_that() {
1848        // The shape of the success fast path's precondition: exact, mask zero, body bit set.
1849        static M0: Schema<'static> =
1850            Schema::new_member(shape_id!("t", "O", "a"), ShapeType::String, "a", 0);
1851        static M1: Schema<'static> =
1852            Schema::new_member(shape_id!("t", "O", "b"), ShapeType::Integer, "b", 1);
1853        static OUTPUT: Schema<'static> =
1854            Schema::new_struct(shape_id!("t", "O"), ShapeType::Structure, &[&M0, &M1]);
1855
1856        assert_eq!(OUTPUT.response_binding_mask(), 0);
1857        assert!(OUTPUT.has_response_body_members());
1858        assert!(!OUTPUT.response_bindings_need_scan());
1859    }
1860
1861    #[test]
1862    fn non_struct_schemas_default_to_the_conservative_word() {
1863        // Zero must read as "nothing bound, nothing in the body", which fails the fast
1864        // path's body-bit requirement and so routes through the composite.
1865        let scalar = Schema::new(shape_id!("t", "S"), ShapeType::String);
1866        assert_eq!(scalar.response_binding_mask(), 0);
1867        assert!(!scalar.has_response_body_members());
1868        assert!(!scalar.response_bindings_need_scan());
1869
1870        let member = Schema::new_member(shape_id!("t", "O", "m"), ShapeType::String, "m", 0);
1871        assert_eq!(member.response_binding_mask(), 0);
1872        assert!(!member.has_response_body_members());
1873    }
1874
1875    #[test]
1876    fn unions_get_a_routing_word_too() {
1877        // Codegen builds union schemas with `new_struct`, so the derivation must accept
1878        // `ShapeType::Union` and the invariant must hold for union members as well.
1879        static V0: Schema<'static> =
1880            Schema::new_member(shape_id!("t", "U", "a"), ShapeType::String, "a", 0);
1881        static V1: Schema<'static> =
1882            Schema::new_member(shape_id!("t", "U", "b"), ShapeType::String, "b", 1);
1883        static UNION: Schema<'static> =
1884            Schema::new_struct(shape_id!("t", "U"), ShapeType::Union, &[&V0, &V1]);
1885        assert_eq!(UNION.response_binding_mask(), 0);
1886        assert!(UNION.has_response_body_members());
1887    }
1888
1889    #[test]
1890    fn mask_and_scan_paths_agree_on_the_same_members() {
1891        // The design requires the exact-mask path and the scan fallback to produce
1892        // identical results. With a hand-built schema we can compare them directly.
1893        static H: Schema<'static> =
1894            Schema::new_member(shape_id!("t", "O", "h"), ShapeType::String, "h", 0)
1895                .with_http_header("x-h");
1896        static B: Schema<'static> =
1897            Schema::new_member(shape_id!("t", "O", "b"), ShapeType::String, "b", 1);
1898        static S: Schema<'static> =
1899            Schema::new_member(shape_id!("t", "O", "s"), ShapeType::Integer, "s", 2)
1900                .with_http_response_code();
1901        static OUTPUT: Schema<'static> =
1902            Schema::new_struct(shape_id!("t", "O"), ShapeType::Structure, &[&H, &B, &S]);
1903
1904        let via_mask = mask_indices(&OUTPUT);
1905        let via_scan: Vec<usize> = OUTPUT
1906            .members()
1907            .iter()
1908            .enumerate()
1909            .filter(|(_, m)| m.has_http_response_binding())
1910            .map(|(i, _)| i)
1911            .collect();
1912        assert_eq!(via_mask, via_scan);
1913    }
1914
1915    #[test]
1916    fn mask_bits_resolve_to_member_schemas_in_o1() {
1917        // What makes the mask useful: a set bit is a direct index into the member slice.
1918        static H: Schema<'static> =
1919            Schema::new_member(shape_id!("t", "O", "h"), ShapeType::String, "h", 0)
1920                .with_http_header("x-wanted");
1921        static B: Schema<'static> =
1922            Schema::new_member(shape_id!("t", "O", "b"), ShapeType::String, "b", 1);
1923        static OUTPUT: Schema<'static> =
1924            Schema::new_struct(shape_id!("t", "O"), ShapeType::Structure, &[&H, &B]);
1925
1926        let indices = mask_indices(&OUTPUT);
1927        assert_eq!(indices, vec![0]);
1928        let member = OUTPUT
1929            .member_schema_by_index(indices[0])
1930            .expect("a mask bit must resolve to a member");
1931        assert_eq!(member.member_name(), Some("h"));
1932        assert_eq!(member.http_header().map(|h| h.value()), Some("x-wanted"));
1933    }
1934
1935    /// Builds a structure schema at runtime with `count` members, marking those whose
1936    /// index is in `bound` with `@httpHeader`. Returns owned storage plus the schema.
1937    ///
1938    /// `new_struct` is a `const fn` but is equally callable at runtime, which is how these
1939    /// tests reach member counts that would be impractical to write out as statics.
1940    fn struct_with(count: usize, bound: &[usize]) -> (Vec<Schema<'static>>, Vec<&'static str>) {
1941        // Member names must outlive the schema, so leak them; this is a test.
1942        let names: Vec<&'static str> = (0..count)
1943            .map(|i| Box::leak(format!("m{i}").into_boxed_str()) as &'static str)
1944            .collect();
1945        let members: Vec<Schema<'static>> = (0..count)
1946            .map(|i| {
1947                let m = Schema::new_member(shape_id!("t", "Big"), ShapeType::String, names[i], i);
1948                if bound.contains(&i) {
1949                    m.with_http_header(names[i])
1950                } else {
1951                    m
1952                }
1953            })
1954            .collect();
1955        (members, names)
1956    }
1957
1958    #[test]
1959    fn a_bound_member_beyond_the_mask_forces_the_scan_fallback() {
1960        // 64 members, all response-bound: positions 62 and 63 cannot be represented, so
1961        // the mask is incomplete and consumers must scan.
1962        let count = 64;
1963        let all: Vec<usize> = (0..count).collect();
1964        let (members, _names) = struct_with(count, &all);
1965        let refs: Vec<&Schema<'static>> = members.iter().collect();
1966        let schema = Schema::new_struct(shape_id!("t", "Big"), ShapeType::Structure, &refs);
1967
1968        assert!(
1969            schema.response_bindings_need_scan(),
1970            "a bound member at position >= 62 must set the scan flag"
1971        );
1972        assert!(
1973            !schema.has_response_body_members(),
1974            "every member is bound, so there is nothing for the body codec"
1975        );
1976
1977        // The scan fallback must find every bound member, including the ones the mask
1978        // could not represent.
1979        let via_scan: Vec<usize> = schema
1980            .members()
1981            .iter()
1982            .enumerate()
1983            .filter(|(_, m)| m.has_http_response_binding())
1984            .map(|(i, _)| i)
1985            .collect();
1986        assert_eq!(via_scan, all, "the scan path must see all 64 bound members");
1987    }
1988
1989    #[test]
1990    fn a_structure_at_the_mask_boundary_stays_exact() {
1991        // Exactly 62 bound members is the largest representable case; it must NOT scan.
1992        let count = crate::response_routing::MASK_BITS;
1993        let all: Vec<usize> = (0..count).collect();
1994        let (members, _names) = struct_with(count, &all);
1995        let refs: Vec<&Schema<'static>> = members.iter().collect();
1996        let schema = Schema::new_struct(shape_id!("t", "Big"), ShapeType::Structure, &refs);
1997
1998        assert!(
1999            !schema.response_bindings_need_scan(),
2000            "62 members fit the mask exactly and must not trigger a scan"
2001        );
2002        assert_eq!(
2003            schema.response_binding_mask().count_ones() as usize,
2004            count,
2005            "every one of the 62 positions should be marked"
2006        );
2007        assert_eq!(mask_indices(&schema), all);
2008    }
2009
2010    #[test]
2011    fn many_members_stay_exact_when_the_bindings_are_within_range() {
2012        // More than 62 members is fine as long as no *bound* member sits out of range: the
2013        // mask still describes every binding, so the fast path is preserved.
2014        let (members, _names) = struct_with(70, &[0, 5, 61]);
2015        let refs: Vec<&Schema<'static>> = members.iter().collect();
2016        let schema = Schema::new_struct(shape_id!("t", "Big"), ShapeType::Structure, &refs);
2017
2018        assert!(!schema.response_bindings_need_scan());
2019        assert_eq!(mask_indices(&schema), vec![0, 5, 61]);
2020        assert!(schema.has_response_body_members());
2021    }
2022
2023    #[test]
2024    fn the_mask_handles_sparse_bindings() {
2025        // The §11.4 sparse layout: a few bound members among many body members.
2026        let (members, _names) = struct_with(50, &[3, 17, 42, 49]);
2027        let refs: Vec<&Schema<'static>> = members.iter().collect();
2028        let schema = Schema::new_struct(shape_id!("t", "Big"), ShapeType::Structure, &refs);
2029
2030        assert_eq!(mask_indices(&schema), vec![3, 17, 42, 49]);
2031        assert!(schema.has_response_body_members());
2032        // O(bound), not O(all): four iterations for a fifty-member structure.
2033        assert_eq!(schema.response_binding_mask().count_ones(), 4);
2034    }
2035
2036    #[test]
2037    #[should_panic(expected = "member-index order")]
2038    fn out_of_order_members_are_rejected() {
2039        // The mask treats a member's array position as its index, so a mismatch would
2040        // silently route the wrong member. In generated code this is a compile error
2041        // because the schema is const-evaluated; here it is a runtime panic.
2042        static A: Schema<'static> =
2043            Schema::new_member(shape_id!("t", "O", "a"), ShapeType::String, "a", 0);
2044        static B: Schema<'static> =
2045            Schema::new_member(shape_id!("t", "O", "b"), ShapeType::String, "b", 1);
2046        // Swapped: B (index 1) is at position 0.
2047        let _ = Schema::new_struct(shape_id!("t", "O"), ShapeType::Structure, &[&B, &A]);
2048    }
2049
2050    #[test]
2051    #[should_panic(expected = "must carry a member index")]
2052    fn a_non_member_in_the_member_array_is_rejected() {
2053        // A schema with no member index cannot be positioned in the mask.
2054        static NOT_A_MEMBER: Schema<'static> = Schema::new(shape_id!("t", "X"), ShapeType::String);
2055        let _ = Schema::new_struct(shape_id!("t", "O"), ShapeType::Structure, &[&NOT_A_MEMBER]);
2056    }
2057}