Skip to main content

aws_smithy_schema/schema/
registry.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Runtime type registry for resolving schemas by `ShapeId`.
7//!
8//! A [`TypeRegistry`] maps shape IDs to a pair of (a) the corresponding
9//! [`Schema`] and (b) a deserialize function that produces a typed value
10//! from a [`ShapeDeserializer`].
11//!
12//! Registries are the runtime piece behind the SEP's "Type Registry" concept.
13//! They support two main use cases:
14//!
15//! 1. **Document → typed shape conversion.** Given a [`Document`](aws_smithy_types::Document) carrying
16//!    a discriminator ([`ShapeId`]), the registry resolves the discriminator
17//!    to a `RegistryEntry`, walks the document via [`DocumentShapeDeserializer`],
18//!    and returns the typed value as a [`TypeErasedBox`] for the caller to
19//!    downcast.
20//! 2. **Schema lookup by shape ID.** Given a [`ShapeId`] (e.g. one extracted
21//!    from an error envelope discriminator), the registry returns the
22//!    corresponding [`Schema`] so generated code can dispatch on it.
23//!
24//! Registries are normally constructed at the package level by code generation
25//! and exposed through `Client::registry()` (per the paired SEP). They can
26//! also be composed via [`TypeRegistry::compose`] to combine, for example,
27//! a service's primary registry with an extension package's registry.
28//!
29//! See the Smithy SEP "Document Types and Type Registries" for the
30//! full specification.
31
32use std::collections::hash_map;
33use std::collections::HashMap;
34use std::fmt;
35
36use aws_smithy_types::type_erasure::TypeErasedBox;
37use aws_smithy_types::DiscriminatedDocument;
38
39use crate::document::DocumentShapeDeserializer;
40use crate::schema::error_envelope::sanitize_error_code;
41use crate::serde::{SerdeError, ShapeDeserializer};
42use crate::Schema;
43use crate::ShapeId;
44
45/// Type-erased deserialize function.
46///
47/// Takes a [`ShapeDeserializer`] positioned at the value to be read and
48/// returns the constructed value as a [`TypeErasedBox`]. The caller
49/// downcasts to recover the concrete type.
50///
51/// Code generation produces one of these per registered shape; the body
52/// is typically a thin wrapper around the shape's generated `deserialize`
53/// method that wraps the result in `TypeErasedBox::new(...)`.
54pub type DeserializeFn = fn(&mut dyn ShapeDeserializer) -> Result<TypeErasedBox, SerdeError>;
55
56/// Error-typed deserialize function.
57///
58/// Like [`DeserializeFn`], but returns the reified value boxed as a
59/// `dyn std::error::Error` so it can be attached as the `source` of an
60/// unhandled error. Error-registry entries carry one of these in addition to
61/// the type-erased [`DeserializeFn`]; primary-registry entries do not.
62pub type ErrorDeserializeFn =
63    fn(&mut dyn ShapeDeserializer) -> Result<Box<dyn std::error::Error + Send + Sync>, SerdeError>;
64
65/// A single entry in a [`TypeRegistry`], pairing a shape's [`Schema`]
66/// with a [`DeserializeFn`] that constructs the typed value.
67///
68/// Error-registry entries additionally carry an [`ErrorDeserializeFn`] so the
69/// reified value can be surfaced as the `source` of an unhandled error; see
70/// [`reify_error`].
71#[derive(Clone, Copy)]
72pub struct RegistryEntry {
73    schema: &'static Schema<'static>,
74    deserialize: DeserializeFn,
75    error_deserialize: Option<ErrorDeserializeFn>,
76}
77
78impl RegistryEntry {
79    /// Build a `RegistryEntry` from a static schema reference and a
80    /// type-erased deserialize function.
81    pub const fn new(schema: &'static Schema<'static>, deserialize: DeserializeFn) -> Self {
82        Self {
83            schema,
84            deserialize,
85            error_deserialize: None,
86        }
87    }
88
89    /// Build a `RegistryEntry` for an `@error` shape, carrying both a
90    /// type-erased [`DeserializeFn`] and an [`ErrorDeserializeFn`] that boxes
91    /// the reified value as a `dyn std::error::Error`.
92    pub const fn new_error(
93        schema: &'static Schema<'static>,
94        deserialize: DeserializeFn,
95        error_deserialize: ErrorDeserializeFn,
96    ) -> Self {
97        Self {
98            schema,
99            deserialize,
100            error_deserialize: Some(error_deserialize),
101        }
102    }
103
104    /// The static schema for this entry's shape.
105    pub fn schema(&self) -> &'static Schema<'static> {
106        self.schema
107    }
108
109    /// The type-erased deserialize function for this entry.
110    pub fn deserialize_fn(&self) -> DeserializeFn {
111        self.deserialize
112    }
113
114    /// The error deserialize function for this entry, if it was registered as
115    /// an `@error` shape.
116    pub fn error_deserialize_fn(&self) -> Option<ErrorDeserializeFn> {
117        self.error_deserialize
118    }
119}
120
121impl fmt::Debug for RegistryEntry {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        // The DeserializeFn pointer is not useful to print; show the shape id.
124        f.debug_struct("RegistryEntry")
125            .field("shape_id", self.schema.shape_id())
126            .finish_non_exhaustive()
127    }
128}
129
130/// A runtime mapping of [`ShapeId`] to [`RegistryEntry`].
131///
132/// Backed by a `HashMap<ShapeId<'static>, RegistryEntry>`. Construct via
133/// [`TypeRegistry::builder`] for the most common pattern, or
134/// [`TypeRegistry::new`]/[`Default::default`] for an empty registry.
135///
136/// Use [`TypeRegistry::compose`] to merge two registries; entries in the
137/// argument registry override entries with the same shape ID in `self`.
138///
139/// # Cross-lifetime lookup
140///
141/// Internal storage is keyed by `ShapeId<'static>` (codegen-emitted shapes
142/// always have static lifetime). The public `schema_for` / `entry_for`
143/// methods accept `&ShapeId<'_>` of any lifetime; lookup is by fully
144/// qualified name via `ShapeId`'s `Borrow<str>` impl. The companion
145/// `*_fqn` methods take `&str` directly — handy after extracting a
146/// `__type` field from a wire-format JSON document.
147#[derive(Default)]
148pub struct TypeRegistry {
149    entries: HashMap<ShapeId<'static>, RegistryEntry>,
150}
151
152impl TypeRegistry {
153    /// Create an empty registry.
154    pub fn new() -> Self {
155        Self::default()
156    }
157
158    /// Start building a registry. See [`TypeRegistryBuilder`].
159    pub fn builder() -> TypeRegistryBuilder {
160        TypeRegistryBuilder {
161            entries: HashMap::new(),
162        }
163    }
164
165    /// Look up the static schema for `id`, or `None` if not registered.
166    ///
167    /// Accepts a [`ShapeId`] of any lifetime — lookup is by fully
168    /// qualified name. `ShapeId<'static>` (codegen-emitted) and a
169    /// runtime-built `ShapeId<'_>` with the same FQN resolve to the
170    /// same entry.
171    pub fn schema_for(&self, id: &ShapeId<'_>) -> Option<&'static Schema<'static>> {
172        self.entries.get(id.as_str()).map(|e| e.schema)
173    }
174
175    /// Look up the static schema for the given fully qualified name
176    /// (e.g. `"smithy.example#Bird"`).
177    ///
178    /// Use this when you already have the FQN as a string slice — for
179    /// instance, after extracting a `__type` discriminator from a
180    /// JSON document.
181    ///
182    /// # Example
183    ///
184    /// ```
185    /// use aws_smithy_schema::prelude;
186    /// use aws_smithy_schema::registry::TypeRegistry;
187    /// use aws_smithy_schema::serde::{SerdeError, ShapeDeserializer};
188    /// use aws_smithy_types::type_erasure::TypeErasedBox;
189    ///
190    /// fn deserialize_string(d: &mut dyn ShapeDeserializer) -> Result<TypeErasedBox, SerdeError> {
191    ///     Ok(TypeErasedBox::new(d.read_string(&prelude::STRING)?))
192    /// }
193    ///
194    /// let registry = TypeRegistry::builder()
195    ///     .insert_shape(&prelude::STRING, deserialize_string)
196    ///     .build();
197    ///
198    /// // FQN extracted at runtime — for instance from a wire-format
199    /// // `__type` field — looks up against the static registry.
200    /// assert!(registry.schema_for_fqn("smithy.api#String").is_some());
201    /// assert!(registry.schema_for_fqn("smithy.api#Boolean").is_none());
202    /// ```
203    pub fn schema_for_fqn(&self, fqn: &str) -> Option<&'static Schema<'static>> {
204        self.entries.get(fqn).map(|e| e.schema)
205    }
206
207    /// Look up the entry (schema + deserialize fn) for `id`, or `None`
208    /// if not registered.
209    ///
210    /// Accepts a [`ShapeId`] of any lifetime; see [`Self::schema_for`].
211    pub fn entry_for(&self, id: &ShapeId<'_>) -> Option<&RegistryEntry> {
212        self.entries.get(id.as_str())
213    }
214
215    /// Look up the entry for the given fully qualified name. See
216    /// [`Self::schema_for_fqn`].
217    pub fn entry_for_fqn(&self, fqn: &str) -> Option<&RegistryEntry> {
218        self.entries.get(fqn)
219    }
220
221    /// Look up the entry for an error shape by its **wire error code**
222    /// (e.g. `"NotFound"`), rather than by fully qualified name.
223    ///
224    /// Error responses identify the error by a bare code on the wire — the
225    /// `<Code>` element for restXml, the `__type` discriminator for the JSON
226    /// protocols, the `x-amzn-query-error` header for awsQueryCompatible — not
227    /// by a fully qualified [`ShapeId`]. This resolves such a code to its entry
228    /// so a schema-serde error path can reify an error by the identifier it
229    /// actually receives off the wire.
230    ///
231    /// `wire_code` is sanitized first (a leading `namespace#` prefix and a
232    /// trailing `:url` suffix are stripped), so a bare name, a fully qualified
233    /// name, or a URL-suffixed code all resolve correctly. Matching is by the
234    /// registered shape's name; per-operation and per-service error sets are
235    /// small, so a linear scan is sufficient and needs no extra generated index.
236    ///
237    /// Returns `None` if no registered shape's name matches the code.
238    //
239    // TODO(schema-error): honor an `@awsQueryError` code override once `Schema`
240    // carries that trait — awsQueryCompatible services can emit a custom error
241    // code that differs from the shape name, which this name-based match would miss.
242    pub fn entry_for_error_code(&self, wire_code: &str) -> Option<&RegistryEntry> {
243        let wire_code = sanitize_error_code(wire_code);
244        self.iter()
245            .find_map(|(id, entry)| (id.shape_name() == wire_code).then_some(entry))
246    }
247
248    /// Deserialize the given `document` into the shape pointed to by its
249    /// discriminator.
250    ///
251    /// Returns `Err(SerdeError::InvalidInput { .. })` if the document has no
252    /// discriminator, and `Err(SerdeError::UnknownMember { .. })` if the
253    /// discriminator does not match a registered shape.
254    ///
255    /// On success, the returned [`TypeErasedBox`] carries an instance of the
256    /// concrete data type that the registered [`DeserializeFn`] produces.
257    /// Callers downcast via [`TypeErasedBox::downcast`].
258    ///
259    /// The document's lifetime is independent of the registry's storage —
260    /// runtime-parsed documents (e.g. with a `__type` discriminator
261    /// borrowed from input bytes) resolve against the `'static`-keyed
262    /// registry via FQN matching.
263    ///
264    /// # Example
265    ///
266    /// ```
267    /// use aws_smithy_schema::prelude;
268    /// use aws_smithy_schema::registry::TypeRegistry;
269    /// use aws_smithy_schema::serde::{SerdeError, ShapeDeserializer};
270    /// use aws_smithy_types::{DiscriminatedDocument, Document};
271    /// use aws_smithy_types::type_erasure::TypeErasedBox;
272    ///
273    /// fn deserialize_string(d: &mut dyn ShapeDeserializer) -> Result<TypeErasedBox, SerdeError> {
274    ///     Ok(TypeErasedBox::new(d.read_string(&prelude::STRING)?))
275    /// }
276    ///
277    /// let registry = TypeRegistry::builder()
278    ///     .insert_shape(&prelude::STRING, deserialize_string)
279    ///     .build();
280    ///
281    /// // Build a discriminated document. In practice this typically comes
282    /// // from `JsonDeserializer::read_discriminated_document` after a
283    /// // wire-format `__type` lift.
284    /// let doc = DiscriminatedDocument::new(Document::String("hi".to_string()))
285    ///     .with_discriminator("smithy.api#String");
286    ///
287    /// let typed = registry.deserialize_document(&doc).unwrap();
288    /// let value = typed.downcast::<String>().unwrap();
289    /// assert_eq!(*value, "hi");
290    /// ```
291    pub fn deserialize_document(
292        &self,
293        document: &DiscriminatedDocument,
294    ) -> Result<TypeErasedBox, SerdeError> {
295        let id = document.discriminator().ok_or_else(|| {
296            SerdeError::invalid_input(
297                "document has no discriminator; cannot resolve shape via TypeRegistry".to_string(),
298            )
299        })?;
300        let entry = self
301            .entries
302            .get(id)
303            .ok_or_else(|| SerdeError::unknown_member(id.to_string()))?;
304        let mut deser = DocumentShapeDeserializer::new_with_settings(
305            document.document(),
306            document.settings().cloned(),
307        );
308        (entry.deserialize)(&mut deser)
309    }
310
311    /// Combine `self` and `other` into a new registry containing the union
312    /// of their entries.
313    ///
314    /// On shape ID collision, entries from `other` override entries in
315    /// `self`. This matches the typical use case where the caller wants
316    /// extension-package entries to take precedence over base-package
317    /// entries.
318    pub fn compose(mut self, other: TypeRegistry) -> TypeRegistry {
319        for (id, entry) in other.entries {
320            self.entries.insert(id, entry);
321        }
322        self
323    }
324
325    /// Borrow `self` and `fallback` into a [`ComposedRegistry`] whose lookups
326    /// consult `self` first and fall back to `fallback`.
327    ///
328    /// Unlike [`compose`](Self::compose), this neither consumes nor copies
329    /// either registry — both are borrowed in place, so two `&'static`
330    /// registries (the typical case for codegen-emitted statics) can be
331    /// layered per call with no allocation.
332    ///
333    /// The precedence — `self` wins over `fallback` on a colliding lookup — is
334    /// what makes this useful for **operation-scoped** error resolution: layer
335    /// an operation's own error registry over the service-wide one so the
336    /// operation's modeled errors take priority before widening to the rest of
337    /// the service closure.
338    pub fn or<'a>(&'a self, fallback: &'a TypeRegistry) -> ComposedRegistry<'a> {
339        ComposedRegistry {
340            primary: self,
341            fallback,
342        }
343    }
344
345    /// Iterate the registry's `(ShapeId, RegistryEntry)` pairs. Iteration
346    /// order is unspecified.
347    pub fn iter(&self) -> Iter<'_> {
348        Iter {
349            inner: self.entries.iter(),
350        }
351    }
352
353    /// Number of registered entries.
354    pub fn len(&self) -> usize {
355        self.entries.len()
356    }
357
358    /// `true` iff the registry has no entries.
359    pub fn is_empty(&self) -> bool {
360        self.entries.is_empty()
361    }
362}
363
364impl fmt::Debug for TypeRegistry {
365    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366        f.debug_struct("TypeRegistry")
367            .field("entries", &self.entries.len())
368            .finish_non_exhaustive()
369    }
370}
371
372/// Two [`TypeRegistry`] references layered for lookup, produced by
373/// [`TypeRegistry::or`].
374///
375/// Lookups consult the `primary` registry first and fall back to the
376/// `fallback` registry, so an entry in `primary` shadows an entry with the
377/// same key in `fallback`. Both registries are borrowed, so a `ComposedRegistry`
378/// is a cheap, allocation-free view — typically a per-operation error registry
379/// layered over the service-wide one for operation-scoped error resolution.
380#[derive(Debug, Clone, Copy)]
381pub struct ComposedRegistry<'a> {
382    primary: &'a TypeRegistry,
383    fallback: &'a TypeRegistry,
384}
385
386impl ComposedRegistry<'_> {
387    /// Look up an entry by [`ShapeId`], consulting `primary` then `fallback`.
388    pub fn entry_for(&self, id: &ShapeId<'_>) -> Option<&RegistryEntry> {
389        self.primary
390            .entry_for(id)
391            .or_else(|| self.fallback.entry_for(id))
392    }
393
394    /// Look up an entry by fully qualified name, consulting `primary` then
395    /// `fallback`.
396    pub fn entry_for_fqn(&self, fqn: &str) -> Option<&RegistryEntry> {
397        self.primary
398            .entry_for_fqn(fqn)
399            .or_else(|| self.fallback.entry_for_fqn(fqn))
400    }
401
402    /// Look up an entry by wire error code, consulting `primary` then
403    /// `fallback`. See [`TypeRegistry::entry_for_error_code`].
404    ///
405    /// This is the operation-scoped error resolution primitive: with an
406    /// operation's error registry as `primary` and the service-wide registry
407    /// as `fallback`, an error the operation models is resolved against the
408    /// operation's own set before the lookup widens to the rest of the service.
409    pub fn entry_for_error_code(&self, wire_code: &str) -> Option<&RegistryEntry> {
410        self.primary
411            .entry_for_error_code(wire_code)
412            .or_else(|| self.fallback.entry_for_error_code(wire_code))
413    }
414}
415
416/// Reify an error response body into a boxed `dyn std::error::Error`, scoped to
417/// an operation and then widened to the service.
418///
419/// Given a `registry` composed as `operation_errors.or(&service_errors)`, a wire
420/// `error_code`, and a [`ShapeDeserializer`] positioned at the error body (such
421/// as the one returned by a protocol's `deserialize_error_response`), this looks
422/// the code up — the operation's own errors first, then the service-wide set —
423/// and runs the matched entry's [`ErrorDeserializeFn`]. The result is the typed
424/// error boxed as a `dyn std::error::Error`, ready to be attached as the
425/// `source` of an unhandled error.
426///
427/// It is the registry-backed fallback for the schema-serde error path, meant to
428/// run only after an operation's modeled-error match misses, to recover
429/// structure from an error the operation does not model but the service (or an
430/// extension package) does. Deserialization failures are swallowed (returning
431/// `None`) so a malformed or unexpected body can never change how an operation
432/// fails relative to the plain generic-error path.
433///
434/// HTTP response bindings on a reified error *are* populated, provided the
435/// caller passes a deserializer obtained from a protocol that owns them. A REST
436/// protocol's `deserialize_error_response` returns a composite that reads
437/// `@httpHeader`, `@httpPrefixHeaders`, and `@httpResponseCode` members from the
438/// response and delegates the remaining members to the body codec, so the
439/// entry's [`ErrorDeserializeFn`] — which is the shape's protocol-agnostic
440/// member consumer — receives values from both sources. Body-only protocols
441/// return a body-only deserializer and those members stay absent, which is the
442/// correct behavior for them.
443///
444/// Returns `None` if `error_code` matches no registered shape, if the matched
445/// entry carries no [`ErrorDeserializeFn`] (i.e. it was not registered as an
446/// `@error` shape), or if deserialization of the matched shape fails.
447pub fn reify_error(
448    registry: ComposedRegistry<'_>,
449    error_code: &str,
450    deser: &mut dyn ShapeDeserializer,
451) -> Option<Box<dyn std::error::Error + Send + Sync>> {
452    let entry = registry.entry_for_error_code(error_code)?;
453    (entry.error_deserialize_fn()?)(deser).ok()
454}
455
456/// Builder for [`TypeRegistry`].
457#[derive(Default)]
458pub struct TypeRegistryBuilder {
459    entries: HashMap<ShapeId<'static>, RegistryEntry>,
460}
461
462impl fmt::Debug for TypeRegistryBuilder {
463    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
464        f.debug_struct("TypeRegistryBuilder")
465            .field("entries", &self.entries.len())
466            .finish_non_exhaustive()
467    }
468}
469
470impl TypeRegistryBuilder {
471    /// Insert an explicit `(ShapeId, RegistryEntry)` pair.
472    pub fn insert(mut self, id: ShapeId<'static>, entry: RegistryEntry) -> Self {
473        self.entries.insert(id, entry);
474        self
475    }
476
477    /// Insert a registration derived from a schema and a deserialize
478    /// function. The shape ID is taken from the schema.
479    ///
480    /// This is the form generated code typically uses.
481    pub fn insert_shape(
482        mut self,
483        schema: &'static Schema<'static>,
484        deserialize: DeserializeFn,
485    ) -> Self {
486        let id = schema.shape_id().clone();
487        self.entries
488            .insert(id, RegistryEntry::new(schema, deserialize));
489        self
490    }
491
492    /// Insert a registration for an `@error` shape, carrying both a type-erased
493    /// [`DeserializeFn`] and an [`ErrorDeserializeFn`]. The shape ID is taken
494    /// from the schema.
495    ///
496    /// This is the form generated code uses for error registries; the
497    /// [`ErrorDeserializeFn`] lets [`reify_error`] surface the reified value as
498    /// the `source` of an unhandled error.
499    pub fn insert_error_shape(
500        mut self,
501        schema: &'static Schema<'static>,
502        deserialize: DeserializeFn,
503        error_deserialize: ErrorDeserializeFn,
504    ) -> Self {
505        let id = schema.shape_id().clone();
506        self.entries.insert(
507            id,
508            RegistryEntry::new_error(schema, deserialize, error_deserialize),
509        );
510        self
511    }
512
513    /// Finalize the builder into a [`TypeRegistry`].
514    pub fn build(self) -> TypeRegistry {
515        TypeRegistry {
516            entries: self.entries,
517        }
518    }
519}
520
521/// Iterator over the entries of a [`TypeRegistry`]. Returned by
522/// [`TypeRegistry::iter`].
523pub struct Iter<'a> {
524    inner: hash_map::Iter<'a, ShapeId<'static>, RegistryEntry>,
525}
526
527impl fmt::Debug for Iter<'_> {
528    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
529        f.debug_struct("Iter").finish_non_exhaustive()
530    }
531}
532
533impl<'a> Iterator for Iter<'a> {
534    type Item = (&'a ShapeId<'static>, &'a RegistryEntry);
535
536    fn next(&mut self) -> Option<Self::Item> {
537        self.inner.next()
538    }
539
540    fn size_hint(&self) -> (usize, Option<usize>) {
541        self.inner.size_hint()
542    }
543}
544
545impl ExactSizeIterator for Iter<'_> {
546    fn len(&self) -> usize {
547        self.inner.len()
548    }
549}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554
555    use aws_smithy_types::{Blob, Document, DocumentError, DocumentSettings, Number};
556
557    use std::sync::Arc;
558
559    use crate::shape_id;
560    use crate::ShapeType;
561
562    // -- Schemas used across tests --------------------------------------------------------------
563
564    static M_FOO_NAME: Schema<'static> = Schema::new_member(
565        shape_id!("smithy.example", "Foo", "name"),
566        ShapeType::String,
567        "name",
568        0,
569    );
570    static FOO_SCHEMA: Schema<'static> = Schema::new_struct(
571        shape_id!("smithy.example", "Foo"),
572        ShapeType::Structure,
573        &[&M_FOO_NAME],
574    );
575
576    static M_BAR_VALUE: Schema<'static> = Schema::new_member(
577        shape_id!("smithy.example", "Bar", "value"),
578        ShapeType::Integer,
579        "value",
580        0,
581    );
582    static BAR_SCHEMA: Schema<'static> = Schema::new_struct(
583        shape_id!("smithy.example", "Bar"),
584        ShapeType::Structure,
585        &[&M_BAR_VALUE],
586    );
587
588    // A structure with a `blob` member, used to verify that protocol
589    // settings are threaded through `deserialize_document` so wire-encoded
590    // blobs (JSON base64 strings) coerce on the type-registry path.
591    static M_WIDGET_DATA: Schema<'static> = Schema::new_member(
592        shape_id!("smithy.example", "Widget", "data"),
593        ShapeType::Blob,
594        "data",
595        0,
596    );
597    static WIDGET_SCHEMA: Schema<'static> = Schema::new_struct(
598        shape_id!("smithy.example", "Widget"),
599        ShapeType::Structure,
600        &[&M_WIDGET_DATA],
601    );
602
603    // -- Test assertion helpers -----------------------------------------------------------------
604
605    /// Assert that `actual` is the same function as `expected`.
606    ///
607    /// This is a no-op under Miri. Miri does not guarantee that a function has a
608    /// single unique address, so two separate reifications of the same `fn` item
609    /// may compare unequal and `fn_addr_eq` would spuriously fail. Gating only
610    /// the comparison (rather than `#[cfg_attr(miri, ignore)]` on the whole test)
611    /// keeps the surrounding assertions running under Miri.
612    fn assert_same_fn(actual: DeserializeFn, expected: DeserializeFn) {
613        #[cfg(not(miri))]
614        assert!(
615            std::ptr::fn_addr_eq(actual, expected),
616            "expected the registered deserialize fn"
617        );
618        #[cfg(miri)]
619        let _ = (actual, expected);
620    }
621
622    // -- Test types and their deserialize fns ---------------------------------------------------
623
624    #[derive(Debug, PartialEq)]
625    struct Foo {
626        name: Option<String>,
627    }
628
629    fn deserialize_foo(deser: &mut dyn ShapeDeserializer) -> Result<TypeErasedBox, SerdeError> {
630        let mut out = Foo { name: None };
631        deser.read_struct(&FOO_SCHEMA, &mut |member, sub| {
632            if let Some(0) = member.member_index() {
633                out.name = Some(sub.read_string(member)?);
634            }
635            Ok(())
636        })?;
637        Ok(TypeErasedBox::new(out))
638    }
639
640    #[derive(Debug, PartialEq)]
641    struct Bar {
642        value: Option<i32>,
643    }
644
645    fn deserialize_bar(deser: &mut dyn ShapeDeserializer) -> Result<TypeErasedBox, SerdeError> {
646        let mut out = Bar { value: None };
647        deser.read_struct(&BAR_SCHEMA, &mut |member, sub| {
648            if let Some(0) = member.member_index() {
649                out.value = Some(sub.read_integer(member)?);
650            }
651            Ok(())
652        })?;
653        Ok(TypeErasedBox::new(out))
654    }
655
656    // An alternative deserialize fn for Foo that returns a different type, used to
657    // verify `compose` override semantics.
658    #[derive(Debug, PartialEq)]
659    struct FooReplacement {
660        replaced: bool,
661    }
662
663    fn deserialize_foo_replacement(
664        _deser: &mut dyn ShapeDeserializer,
665    ) -> Result<TypeErasedBox, SerdeError> {
666        Ok(TypeErasedBox::new(FooReplacement { replaced: true }))
667    }
668
669    #[derive(Debug, PartialEq)]
670    struct Widget {
671        data: Option<Blob>,
672    }
673
674    fn deserialize_widget(deser: &mut dyn ShapeDeserializer) -> Result<TypeErasedBox, SerdeError> {
675        let mut out = Widget { data: None };
676        deser.read_struct(&WIDGET_SCHEMA, &mut |member, sub| {
677            if let Some(0) = member.member_index() {
678                out.data = Some(sub.read_blob(member)?);
679            }
680            Ok(())
681        })?;
682        Ok(TypeErasedBox::new(out))
683    }
684
685    // A minimal JSON-like settings impl that base64-decodes strings into
686    // blob bytes, the way the JSON codec does on the wire. Used to prove
687    // that `deserialize_document` threads protocol settings into the
688    // deserializer so blob/timestamp coercion works on the registry path.
689    #[derive(Debug)]
690    struct JsonishSettings;
691
692    impl DocumentSettings for JsonishSettings {
693        fn protocol_id(&self) -> &str {
694            "smithy.example#Jsonish"
695        }
696
697        fn coerce_string_to_blob(&self, s: &str) -> Result<Vec<u8>, DocumentError> {
698            aws_smithy_types::base64::decode(s)
699                .map_err(|e| DocumentError::unsupported(format!("invalid base64 blob: {e}")))
700        }
701    }
702
703    // -- Lookup tests ---------------------------------------------------------------------------
704
705    #[test]
706    fn schema_for_returns_registered_schema() {
707        let registry = TypeRegistry::builder()
708            .insert_shape(&FOO_SCHEMA, deserialize_foo)
709            .insert_shape(&BAR_SCHEMA, deserialize_bar)
710            .build();
711
712        let foo = registry
713            .schema_for(&shape_id!("smithy.example", "Foo"))
714            .unwrap();
715        assert_eq!(foo.shape_id(), FOO_SCHEMA.shape_id());
716
717        let bar = registry
718            .schema_for(&shape_id!("smithy.example", "Bar"))
719            .unwrap();
720        assert_eq!(bar.shape_id(), BAR_SCHEMA.shape_id());
721    }
722
723    #[test]
724    fn schema_for_returns_none_for_unregistered_shape() {
725        let registry = TypeRegistry::builder()
726            .insert_shape(&FOO_SCHEMA, deserialize_foo)
727            .build();
728
729        assert!(registry
730            .schema_for(&shape_id!("smithy.example", "DoesNotExist"))
731            .is_none());
732    }
733
734    #[test]
735    fn entry_for_returns_schema_and_fn() {
736        let registry = TypeRegistry::builder()
737            .insert_shape(&FOO_SCHEMA, deserialize_foo)
738            .build();
739
740        let entry = registry
741            .entry_for(&shape_id!("smithy.example", "Foo"))
742            .unwrap();
743        assert_eq!(entry.schema().shape_id(), FOO_SCHEMA.shape_id());
744        // Function pointer comparison: same fn we registered.
745        assert_same_fn(entry.deserialize_fn(), deserialize_foo as DeserializeFn);
746    }
747
748    #[test]
749    fn entry_for_error_code_resolves_sanitized_codes() {
750        let registry = TypeRegistry::builder()
751            .insert_shape(&FOO_SCHEMA, deserialize_foo)
752            .insert_shape(&BAR_SCHEMA, deserialize_bar)
753            .build();
754
755        // A bare wire code, a fully qualified code, a URL-suffixed code, and a
756        // fully qualified + URL-suffixed code all resolve to the same entry.
757        for code in [
758            "Foo",
759            "smithy.example#Foo",
760            "Foo:http://internal.example.com/x",
761            "smithy.example#Foo:http://internal.example.com/x",
762        ] {
763            let entry = registry
764                .entry_for_error_code(code)
765                .unwrap_or_else(|| panic!("expected a match for {code:?}"));
766            assert_eq!(entry.schema().shape_id(), FOO_SCHEMA.shape_id());
767        }
768
769        // The scan selects the correct entry among several.
770        let bar = registry.entry_for_error_code("Bar").unwrap();
771        assert_eq!(bar.schema().shape_id(), BAR_SCHEMA.shape_id());
772    }
773
774    #[test]
775    fn entry_for_error_code_returns_none_for_unknown_code() {
776        let registry = TypeRegistry::builder()
777            .insert_shape(&FOO_SCHEMA, deserialize_foo)
778            .build();
779
780        assert!(registry.entry_for_error_code("DoesNotExist").is_none());
781        // A namespace that happens to share the name's text must not match on
782        // the namespace component — matching is on the shape name only.
783        assert!(registry.entry_for_error_code("Foo#Nope").is_none());
784    }
785
786    #[test]
787    fn len_and_is_empty() {
788        let empty = TypeRegistry::new();
789        assert_eq!(empty.len(), 0);
790        assert!(empty.is_empty());
791
792        let populated = TypeRegistry::builder()
793            .insert_shape(&FOO_SCHEMA, deserialize_foo)
794            .insert_shape(&BAR_SCHEMA, deserialize_bar)
795            .build();
796        assert_eq!(populated.len(), 2);
797        assert!(!populated.is_empty());
798    }
799
800    #[test]
801    fn iter_yields_all_entries() {
802        let registry = TypeRegistry::builder()
803            .insert_shape(&FOO_SCHEMA, deserialize_foo)
804            .insert_shape(&BAR_SCHEMA, deserialize_bar)
805            .build();
806
807        let mut ids: Vec<String> = registry.iter().map(|(id, _)| id.to_string()).collect();
808        ids.sort();
809        assert_eq!(
810            ids,
811            vec![
812                "smithy.example#Bar".to_string(),
813                "smithy.example#Foo".to_string(),
814            ]
815        );
816        // ExactSizeIterator: len() agrees with registry len.
817        assert_eq!(registry.iter().len(), 2);
818    }
819
820    // -- Composition tests ----------------------------------------------------------------------
821
822    #[test]
823    fn compose_unions_disjoint_registries() {
824        let a = TypeRegistry::builder()
825            .insert_shape(&FOO_SCHEMA, deserialize_foo)
826            .build();
827        let b = TypeRegistry::builder()
828            .insert_shape(&BAR_SCHEMA, deserialize_bar)
829            .build();
830
831        let merged = a.compose(b);
832        assert_eq!(merged.len(), 2);
833        assert!(merged
834            .schema_for(&shape_id!("smithy.example", "Foo"))
835            .is_some());
836        assert!(merged
837            .schema_for(&shape_id!("smithy.example", "Bar"))
838            .is_some());
839    }
840
841    #[test]
842    fn compose_lets_other_override_self() {
843        // `a` registers `Foo` → deserialize_foo (returns Foo)
844        // `b` registers `Foo` → deserialize_foo_replacement (returns FooReplacement)
845        // After compose, `Foo` should resolve to the replacement.
846        let a = TypeRegistry::builder()
847            .insert_shape(&FOO_SCHEMA, deserialize_foo)
848            .build();
849        let b = TypeRegistry::builder()
850            .insert_shape(&FOO_SCHEMA, deserialize_foo_replacement)
851            .build();
852
853        let merged = a.compose(b);
854        assert_eq!(merged.len(), 1);
855
856        // Resolve Foo and round-trip through deserialize_document.
857        let doc = DiscriminatedDocument::new(Document::Object(Default::default()))
858            .with_discriminator(FOO_SCHEMA.shape_id().as_str());
859        let boxed = merged.deserialize_document(&doc).unwrap();
860        let result = *boxed.downcast::<FooReplacement>().expect("override fn ran");
861        assert_eq!(result, FooReplacement { replaced: true });
862    }
863
864    #[test]
865    fn composed_registry_prefers_primary_then_widens() {
866        // `primary` models Foo (deserialize_foo); `fallback` also has Foo but
867        // with a different fn, plus Bar. On the Foo collision the primary must
868        // win (the opposite precedence of `compose`, where the argument wins),
869        // and the lookup must widen to the fallback for Bar.
870        let primary = TypeRegistry::builder()
871            .insert_shape(&FOO_SCHEMA, deserialize_foo)
872            .build();
873        let fallback = TypeRegistry::builder()
874            .insert_shape(&FOO_SCHEMA, deserialize_foo_replacement)
875            .insert_shape(&BAR_SCHEMA, deserialize_bar)
876            .build();
877        let composed = primary.or(&fallback);
878
879        // Collision: primary wins — the entry carries deserialize_foo, not the
880        // fallback's replacement fn.
881        let foo = composed.entry_for_error_code("Foo").unwrap();
882        assert_same_fn(foo.deserialize_fn(), deserialize_foo as DeserializeFn);
883
884        // Widen: Bar resolves from the fallback.
885        let bar = composed.entry_for_error_code("Bar").unwrap();
886        assert_eq!(bar.schema().shape_id(), BAR_SCHEMA.shape_id());
887
888        // Miss in both: None.
889        assert!(composed.entry_for_error_code("Nope").is_none());
890    }
891
892    #[test]
893    fn composed_registry_entry_for_and_fqn_compose() {
894        let primary = TypeRegistry::builder()
895            .insert_shape(&FOO_SCHEMA, deserialize_foo)
896            .build();
897        let fallback = TypeRegistry::builder()
898            .insert_shape(&BAR_SCHEMA, deserialize_bar)
899            .build();
900        let composed = primary.or(&fallback);
901
902        // entry_for (by ShapeId): primary hit, then fallback widen.
903        assert_eq!(
904            composed
905                .entry_for(&shape_id!("smithy.example", "Foo"))
906                .unwrap()
907                .schema()
908                .shape_id(),
909            FOO_SCHEMA.shape_id()
910        );
911        assert_eq!(
912            composed
913                .entry_for(&shape_id!("smithy.example", "Bar"))
914                .unwrap()
915                .schema()
916                .shape_id(),
917            BAR_SCHEMA.shape_id()
918        );
919
920        // entry_for_fqn: same composition, plus a miss in both.
921        assert!(composed.entry_for_fqn("smithy.example#Foo").is_some());
922        assert!(composed.entry_for_fqn("smithy.example#Bar").is_some());
923        assert!(composed.entry_for_fqn("smithy.example#Nope").is_none());
924    }
925
926    // Error-constructor stubs for `reify_error` tests. They ignore the
927    // deserializer and return a boxed error whose `Display` identifies which
928    // registry the entry came from, so the tests can assert scoping/precedence.
929    fn error_ctor_primary(
930        _deser: &mut dyn ShapeDeserializer,
931    ) -> Result<Box<dyn std::error::Error + Send + Sync>, SerdeError> {
932        Ok("primary".into())
933    }
934
935    fn error_ctor_fallback(
936        _deser: &mut dyn ShapeDeserializer,
937    ) -> Result<Box<dyn std::error::Error + Send + Sync>, SerdeError> {
938        Ok("fallback".into())
939    }
940
941    // An error constructor that always fails, used to prove `reify_error`
942    // swallows deserialize errors.
943    fn error_ctor_always_errors(
944        _deser: &mut dyn ShapeDeserializer,
945    ) -> Result<Box<dyn std::error::Error + Send + Sync>, SerdeError> {
946        Err(SerdeError::custom("boom"))
947    }
948
949    #[test]
950    fn reify_error_scopes_to_primary_then_widens() {
951        // `primary` models Foo (→ "primary"); `fallback` models Bar (→ "fallback").
952        // Each entry carries the input-ignoring type-erased stub plus an error
953        // constructor, so the test exercises reify_error's plumbing and scoping
954        // rather than any specific body shape.
955        let primary = TypeRegistry::builder()
956            .insert_error_shape(&FOO_SCHEMA, deserialize_foo_replacement, error_ctor_primary)
957            .build();
958        let fallback = TypeRegistry::builder()
959            .insert_error_shape(
960                &BAR_SCHEMA,
961                deserialize_foo_replacement,
962                error_ctor_fallback,
963            )
964            .build();
965        let composed = primary.or(&fallback);
966        let doc = Document::Object(Default::default());
967
968        // Operation-scoped hit (primary).
969        let boxed = reify_error(composed, "Foo", &mut DocumentShapeDeserializer::new(&doc))
970            .expect("Foo reifies from the operation registry");
971        assert_eq!(boxed.to_string(), "primary");
972
973        // Widen to the service registry for a code the operation does not model.
974        let boxed = reify_error(composed, "Bar", &mut DocumentShapeDeserializer::new(&doc))
975            .expect("Bar widens to the service registry");
976        assert_eq!(boxed.to_string(), "fallback");
977
978        // Unknown code in both: None.
979        assert!(reify_error(composed, "Nope", &mut DocumentShapeDeserializer::new(&doc)).is_none());
980    }
981
982    #[test]
983    fn reify_error_swallows_deserialize_failure() {
984        // The code resolves, but the matched entry's error constructor errors —
985        // reify_error must return None rather than propagate, so a malformed
986        // unmodeled error can never change how the operation fails.
987        let primary = TypeRegistry::builder()
988            .insert_error_shape(
989                &FOO_SCHEMA,
990                deserialize_foo_replacement,
991                error_ctor_always_errors,
992            )
993            .build();
994        let empty = TypeRegistry::new();
995        let composed = primary.or(&empty);
996        let doc = Document::Object(Default::default());
997
998        assert!(reify_error(composed, "Foo", &mut DocumentShapeDeserializer::new(&doc)).is_none());
999    }
1000
1001    // -- deserialize_document tests -------------------------------------------------------------
1002
1003    #[test]
1004    fn deserialize_document_round_trip() {
1005        let registry = TypeRegistry::builder()
1006            .insert_shape(&FOO_SCHEMA, deserialize_foo)
1007            .insert_shape(&BAR_SCHEMA, deserialize_bar)
1008            .build();
1009
1010        // Build a Foo document with a "name" member and the Foo discriminator.
1011        let mut foo_members = std::collections::HashMap::new();
1012        foo_members.insert("name".to_string(), Document::String("hello".to_string()));
1013        let foo_doc = DiscriminatedDocument::new(Document::Object(foo_members))
1014            .with_discriminator(FOO_SCHEMA.shape_id().as_str());
1015
1016        let boxed = registry.deserialize_document(&foo_doc).unwrap();
1017        let foo = *boxed.downcast::<Foo>().expect("downcast to Foo");
1018        assert_eq!(
1019            foo,
1020            Foo {
1021                name: Some("hello".to_string())
1022            }
1023        );
1024
1025        // Same for Bar.
1026        let mut bar_members = std::collections::HashMap::new();
1027        bar_members.insert("value".to_string(), Document::Number(Number::PosInt(42)));
1028        let bar_doc = DiscriminatedDocument::new(Document::Object(bar_members))
1029            .with_discriminator(BAR_SCHEMA.shape_id().as_str());
1030
1031        let boxed = registry.deserialize_document(&bar_doc).unwrap();
1032        let bar = *boxed.downcast::<Bar>().expect("downcast to Bar");
1033        assert_eq!(bar, Bar { value: Some(42) });
1034    }
1035
1036    #[test]
1037    fn deserialize_document_errors_when_discriminator_missing() {
1038        let registry = TypeRegistry::builder()
1039            .insert_shape(&FOO_SCHEMA, deserialize_foo)
1040            .build();
1041
1042        // No discriminator attached.
1043        let doc = DiscriminatedDocument::new(Document::Object(Default::default()));
1044
1045        let err = registry.deserialize_document(&doc).unwrap_err();
1046        match err {
1047            SerdeError::InvalidInput { message } => {
1048                assert!(
1049                    message.contains("discriminator"),
1050                    "expected message to mention discriminator, got: {message}"
1051                );
1052            }
1053            other => panic!("expected InvalidInput, got {other:?}"),
1054        }
1055    }
1056
1057    #[test]
1058    fn deserialize_document_errors_when_shape_id_unregistered() {
1059        let registry = TypeRegistry::builder()
1060            .insert_shape(&FOO_SCHEMA, deserialize_foo)
1061            .build();
1062
1063        let doc = DiscriminatedDocument::new(Document::Object(Default::default()))
1064            .with_discriminator("smithy.example#Unregistered");
1065
1066        let err = registry.deserialize_document(&doc).unwrap_err();
1067        match err {
1068            SerdeError::UnknownMember { member_name } => {
1069                assert_eq!(member_name, "smithy.example#Unregistered");
1070            }
1071            other => panic!("expected UnknownMember, got {other:?}"),
1072        }
1073    }
1074
1075    #[test]
1076    fn deserialize_document_propagates_inner_error() {
1077        // Foo expects `name` to be a String; supplying an integer will trigger a
1078        // type-mismatch from the deserialize fn.
1079        let registry = TypeRegistry::builder()
1080            .insert_shape(&FOO_SCHEMA, deserialize_foo)
1081            .build();
1082
1083        let mut foo_members = std::collections::HashMap::new();
1084        foo_members.insert("name".to_string(), Document::Number(Number::PosInt(5)));
1085        let doc = DiscriminatedDocument::new(Document::Object(foo_members))
1086            .with_discriminator(FOO_SCHEMA.shape_id().as_str());
1087
1088        let err = registry.deserialize_document(&doc).unwrap_err();
1089        match err {
1090            SerdeError::TypeMismatch { .. } => {}
1091            other => panic!("expected TypeMismatch, got {other:?}"),
1092        }
1093    }
1094
1095    #[test]
1096    fn deserialize_document_threads_settings_for_blob_coercion() {
1097        let registry = TypeRegistry::builder()
1098            .insert_shape(&WIDGET_SCHEMA, deserialize_widget)
1099            .build();
1100
1101        // JSON-origin document: the blob arrives as a base64 string and the
1102        // attached settings know how to decode it. This mirrors what
1103        // `JsonDeserializer::read_discriminated_document` produces. The fix
1104        // threads these settings into the deserializer so the nested
1105        // `read_blob` can coerce.
1106        let mut members = std::collections::HashMap::new();
1107        members.insert("data".to_string(), Document::String("YWJjZA==".to_string()));
1108        let doc = DiscriminatedDocument::new(Document::Object(members))
1109            .with_discriminator(WIDGET_SCHEMA.shape_id().as_str())
1110            .with_settings(Arc::new(JsonishSettings));
1111
1112        let boxed = registry
1113            .deserialize_document(&doc)
1114            .expect("blob coercion should succeed when settings are threaded through");
1115        let widget = *boxed.downcast::<Widget>().expect("downcast to Widget");
1116        assert_eq!(
1117            widget,
1118            Widget {
1119                data: Some(Blob::new(b"abcd".to_vec())),
1120            }
1121        );
1122    }
1123
1124    #[test]
1125    fn deserialize_document_without_settings_uses_base64_default_for_blob() {
1126        // Same base64-string blob, but no settings attached. `Document`
1127        // has no blob variant, so a `blob` member always arrives as a
1128        // string; with no protocol settings the deserializer falls back
1129        // to the deterministic standard-base64 decode, which is what
1130        // `DocumentShapeSerializer::write_blob` produces.
1131        let registry = TypeRegistry::builder()
1132            .insert_shape(&WIDGET_SCHEMA, deserialize_widget)
1133            .build();
1134
1135        let mut members = std::collections::HashMap::new();
1136        members.insert("data".to_string(), Document::String("YWJjZA==".to_string()));
1137        let doc = DiscriminatedDocument::new(Document::Object(members))
1138            .with_discriminator(WIDGET_SCHEMA.shape_id().as_str());
1139
1140        let widget: Widget = *registry
1141            .deserialize_document(&doc)
1142            .expect("base64 default should decode without settings")
1143            .downcast()
1144            .expect("registered shape downcasts to Widget");
1145        assert_eq!(widget.data.unwrap().as_ref(), b"abcd");
1146    }
1147
1148    #[test]
1149    fn deserialize_document_without_settings_rejects_malformed_base64_blob() {
1150        // The base64 fallback still validates: a string that is not
1151        // base64 is reported rather than silently accepted.
1152        let registry = TypeRegistry::builder()
1153            .insert_shape(&WIDGET_SCHEMA, deserialize_widget)
1154            .build();
1155
1156        let mut members = std::collections::HashMap::new();
1157        members.insert(
1158            "data".to_string(),
1159            Document::String("not base64!!!".to_string()),
1160        );
1161        let doc = DiscriminatedDocument::new(Document::Object(members))
1162            .with_discriminator(WIDGET_SCHEMA.shape_id().as_str());
1163
1164        let err = registry.deserialize_document(&doc).unwrap_err();
1165        let msg = format!("{err}");
1166        assert!(msg.contains("base64"), "unexpected error message: {msg}");
1167    }
1168
1169    // -- Builder + Debug tests ------------------------------------------------------------------
1170
1171    #[test]
1172    fn builder_insert_keyed_form() {
1173        // Verify that the keyed `insert(id, entry)` form works alongside `insert_shape`.
1174        let id = FOO_SCHEMA.shape_id();
1175        let registry = TypeRegistry::builder()
1176            .insert(id.clone(), RegistryEntry::new(&FOO_SCHEMA, deserialize_foo))
1177            .build();
1178
1179        assert_eq!(registry.len(), 1);
1180        assert!(registry.schema_for(id).is_some());
1181    }
1182
1183    #[test]
1184    fn debug_impls_do_not_panic() {
1185        let registry = TypeRegistry::builder()
1186            .insert_shape(&FOO_SCHEMA, deserialize_foo)
1187            .build();
1188        let entry = registry.entry_for(FOO_SCHEMA.shape_id()).unwrap();
1189
1190        let _ = format!("{registry:?}");
1191        let _ = format!("{entry:?}");
1192    }
1193
1194    // -- Cross-lifetime lookup tests ----------------------------------------------------------
1195
1196    #[test]
1197    fn schema_for_works_with_runtime_shape_id() {
1198        let registry = TypeRegistry::builder()
1199            .insert_shape(&FOO_SCHEMA, deserialize_foo)
1200            .build();
1201
1202        // Build a runtime ShapeId<'_> with the same FQN as a registered
1203        // 'static shape. The registry should resolve it via the
1204        // FQN-based `Borrow<str>` lookup.
1205        let owned_fqn = String::from("smithy.example#Foo");
1206        let owned_ns = String::from("smithy.example");
1207        let owned_name = String::from("Foo");
1208        let runtime_id: ShapeId<'_> = ShapeId::from_parts(&owned_fqn, &owned_ns, &owned_name);
1209
1210        let schema = registry.schema_for(&runtime_id).expect("found via FQN");
1211        assert_eq!(schema.shape_id(), FOO_SCHEMA.shape_id());
1212
1213        // Negative case: same lifetime, different FQN.
1214        let owned_other = String::from("smithy.example#Nope");
1215        let runtime_miss: ShapeId<'_> = ShapeId::from_parts(&owned_other, &owned_ns, &owned_name);
1216        assert!(registry.schema_for(&runtime_miss).is_none());
1217    }
1218
1219    #[test]
1220    fn schema_for_fqn_returns_registered_schema() {
1221        let registry = TypeRegistry::builder()
1222            .insert_shape(&FOO_SCHEMA, deserialize_foo)
1223            .insert_shape(&BAR_SCHEMA, deserialize_bar)
1224            .build();
1225
1226        let foo = registry
1227            .schema_for_fqn("smithy.example#Foo")
1228            .expect("registered");
1229        assert_eq!(foo.shape_id(), FOO_SCHEMA.shape_id());
1230
1231        assert!(registry.schema_for_fqn("smithy.example#Missing").is_none());
1232    }
1233
1234    #[test]
1235    fn entry_for_fqn_returns_registered_entry() {
1236        let registry = TypeRegistry::builder()
1237            .insert_shape(&FOO_SCHEMA, deserialize_foo)
1238            .build();
1239
1240        let entry = registry
1241            .entry_for_fqn("smithy.example#Foo")
1242            .expect("registered");
1243        assert_same_fn(entry.deserialize_fn(), deserialize_foo as DeserializeFn);
1244    }
1245}