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}