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}