Skip to main content

aws_smithy_schema/schema/
shape_id.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use std::fmt;
7
8/// Creates a [`ShapeId`] from a namespace and shape name at compile time.
9///
10/// The fully qualified name (`namespace#ShapeName`) is computed via `concat!`,
11/// eliminating the risk of the FQN getting out of sync with the parts.
12///
13/// # Examples
14/// ```
15/// use aws_smithy_schema::{shape_id, ShapeId};
16///
17/// const ID: ShapeId<'static> = shape_id!("smithy.api", "String");
18/// assert_eq!(ID.as_str(), "smithy.api#String");
19/// ```
20#[macro_export]
21macro_rules! shape_id {
22    ($ns:literal, $name:literal) => {
23        $crate::ShapeId::from_parts(concat!($ns, "#", $name), $ns, $name)
24    };
25    ($ns:literal, $name:literal, $member:literal) => {
26        $crate::ShapeId::from_parts_with_member(
27            concat!($ns, "#", $name, "$", $member),
28            $ns,
29            $name,
30            $member,
31        )
32    };
33}
34
35/// A Smithy Shape ID, parameterized over the lifetime of its component
36/// strings.
37///
38/// `ShapeId<'static>` is the codegen-emitted form (zero-allocation,
39/// const-constructible via the [`shape_id!`] macro). `ShapeId<'a>` for
40/// shorter `'a` is constructible from runtime-parsed bytes — for example,
41/// from a wire-format `__type` field on a JSON response.
42///
43/// Use the [`shape_id!`] macro to construct `'static` instances — it
44/// computes the fully qualified name at compile time from the namespace
45/// and shape name, preventing the parts from getting out of sync.
46///
47/// # Examples
48/// ```
49/// use aws_smithy_schema::{shape_id, ShapeId};
50///
51/// const ID: ShapeId<'static> = shape_id!("smithy.api", "String");
52/// assert_eq!(ID.namespace(), "smithy.api");
53/// assert_eq!(ID.shape_name(), "String");
54/// assert_eq!(ID.as_str(), "smithy.api#String");
55/// ```
56///
57/// # Variance
58///
59/// `ShapeId<'a>` is **covariant** in `'a` because every field is of the
60/// form `&'a str`. This means a `ShapeId<'static>` (codegen-emitted) is
61/// usable everywhere a `ShapeId<'a>` is expected, regardless of `'a`.
62///
63/// # Equality, hashing, and borrow
64///
65/// Two `ShapeId`s compare equal when their fully qualified names
66/// ([`Self::as_str`]) compare equal. The other fields are determined by
67/// the FQN for any `ShapeId` constructed via the [`shape_id!`] macro or
68/// [`Self::from_parts`] with consistent parts, so this is the same
69/// equality you would get from comparing every field.
70///
71/// FQN-based equality lets [`ShapeId`] act as `Borrow<str>`: a
72/// `HashMap<ShapeId<'static>, V>` can be looked up by `&str` (or by
73/// any `&ShapeId<'_>` via [`Self::as_str`]) without going through
74/// the `'static`-bound storage key. This is what the cross-lifetime
75/// lookup APIs on `TypeRegistry` and `TraitMap` rely on.
76#[derive(Debug, Clone)]
77pub struct ShapeId<'a> {
78    fqn: &'a str,
79    namespace: &'a str,
80    shape_name: &'a str,
81    member_name: Option<&'a str>,
82}
83
84impl PartialEq for ShapeId<'_> {
85    /// Two `ShapeId`s are equal iff their fully qualified names match.
86    /// See the type-level docs for why this is sound.
87    fn eq(&self, other: &Self) -> bool {
88        self.fqn == other.fqn
89    }
90}
91
92impl Eq for ShapeId<'_> {}
93
94impl std::hash::Hash for ShapeId<'_> {
95    /// Hashes the fully qualified name so that `hash(id)` agrees with
96    /// `hash(id.as_str())` — required for the `Borrow<str>` impl.
97    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
98        self.fqn.hash(state);
99    }
100}
101
102impl std::borrow::Borrow<str> for ShapeId<'_> {
103    /// Borrows the fully qualified name. Combined with the FQN-based
104    /// `Hash`/`Eq` impls, this lets `HashMap<ShapeId<'static>, V>::get`
105    /// accept `&str` keys, enabling cross-lifetime lookup against a
106    /// `'static`-keyed table.
107    fn borrow(&self) -> &str {
108        self.fqn
109    }
110}
111
112impl<'a> ShapeId<'a> {
113    /// Creates a `ShapeId` from its already-split component strings.
114    ///
115    /// `const`-callable, so the [`shape_id!`] macro and codegen use it for
116    /// `'static` ids; it also accepts borrowed parts for runtime-built ids, such
117    /// as ids borrowed from a model loaded at runtime or a wire-format `__type`.
118    ///
119    /// The parts must be consistent: `fqn` must be exactly
120    /// `{namespace}#{shape_name}`. This is not checked. Equality, hashing, and
121    /// `Borrow<str>` use only `fqn`, while [`Self::namespace`] and
122    /// [`Self::shape_name`] return the parts as given, so inconsistent parts
123    /// produce an id whose accessors disagree with its identity.
124    ///
125    /// Prefer the [`shape_id!`] macro for literal ids so the parts cannot get
126    /// out of sync.
127    ///
128    /// ```
129    /// use aws_smithy_schema::ShapeId;
130    ///
131    /// let fqn = String::from("com.example#Thing");
132    /// let (namespace, name) = fqn.split_once('#').unwrap();
133    /// let id = ShapeId::from_parts(&fqn, namespace, name);
134    /// assert_eq!(id.namespace(), "com.example");
135    /// assert_eq!(id.shape_name(), "Thing");
136    /// ```
137    pub const fn from_parts(fqn: &'a str, namespace: &'a str, shape_name: &'a str) -> Self {
138        Self {
139            fqn,
140            namespace,
141            shape_name,
142            member_name: None,
143        }
144    }
145
146    /// Creates a member `ShapeId` from its already-split component strings,
147    /// without allocating or validating.
148    ///
149    /// This is the member-shape variant of [`Self::from_parts`], with the same
150    /// consistency requirement: `fqn` must be exactly
151    /// `{namespace}#{shape_name}${member_name}`.
152    ///
153    /// Prefer the [`shape_id!`] macro for literal ids so the parts cannot get
154    /// out of sync.
155    #[doc(hidden)]
156    pub const fn from_parts_with_member(
157        fqn: &'a str,
158        namespace: &'a str,
159        shape_name: &'a str,
160        member_name: &'a str,
161    ) -> Self {
162        Self {
163            fqn,
164            namespace,
165            shape_name,
166            member_name: Some(member_name),
167        }
168    }
169
170    /// Returns the fully qualified string representation (e.g. `"smithy.api#String"`).
171    ///
172    /// The return type is `&'a str` (the lifetime of the data, not the
173    /// receiver). Calling this on `ShapeId<'static>` yields `&'static str`,
174    /// preserving the codegen-side `'static` accessor guarantee.
175    pub const fn as_str(&self) -> &'a str {
176        self.fqn
177    }
178
179    /// Returns the namespace portion of the ShapeId.
180    ///
181    /// Lifetime is `'a` — see [`Self::as_str`] for rationale.
182    pub const fn namespace(&self) -> &'a str {
183        self.namespace
184    }
185
186    /// Returns the shape name portion of the ShapeId.
187    ///
188    /// Lifetime is `'a` — see [`Self::as_str`] for rationale.
189    pub const fn shape_name(&self) -> &'a str {
190        self.shape_name
191    }
192
193    /// Returns the member name if this is a member shape ID.
194    ///
195    /// Lifetime is `'a` — see [`Self::as_str`] for rationale.
196    pub fn member_name(&self) -> Option<&'a str> {
197        self.member_name
198    }
199}
200
201impl fmt::Display for ShapeId<'_> {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        f.write_str(self.fqn)
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_shape_id_macro() {
213        const ID: ShapeId<'static> = shape_id!("smithy.api", "String");
214        assert_eq!(ID.as_str(), "smithy.api#String");
215        assert_eq!(ID.namespace(), "smithy.api");
216        assert_eq!(ID.shape_name(), "String");
217        assert_eq!(ID.member_name(), None);
218    }
219
220    #[test]
221    fn test_shape_id_macro_with_member() {
222        const ID: ShapeId<'static> = shape_id!("com.example", "MyStruct", "field");
223        assert_eq!(ID.as_str(), "com.example#MyStruct$field");
224        assert_eq!(ID.namespace(), "com.example");
225        assert_eq!(ID.shape_name(), "MyStruct");
226        assert_eq!(ID.member_name(), Some("field"));
227    }
228
229    #[test]
230    fn test_display() {
231        let id = shape_id!("smithy.api", "String");
232        assert_eq!(format!("{id}"), "smithy.api#String");
233    }
234
235    #[test]
236    fn test_equality() {
237        let a = shape_id!("smithy.api", "String");
238        let b = shape_id!("smithy.api", "String");
239        assert_eq!(a, b);
240
241        let c = shape_id!("smithy.api", "String", "foo");
242        let d = shape_id!("smithy.api", "String", "foo");
243        assert_eq!(c, d);
244    }
245
246    /// Construct a `ShapeId<'a>` from non-`'static` data. Compile-tests
247    /// that the lifetime parameter does what we want — without this the
248    /// runtime-built ShapeId path would not be exercised by tests.
249    #[test]
250    fn test_runtime_lifetime() {
251        let fqn = String::from("ns#Foo");
252        let ns = String::from("ns");
253        let name = String::from("Foo");
254        let id: ShapeId<'_> = ShapeId::from_parts(&fqn, &ns, &name);
255        assert_eq!(id.as_str(), "ns#Foo");
256        assert_eq!(id.namespace(), "ns");
257        assert_eq!(id.shape_name(), "Foo");
258    }
259
260    /// `ShapeId<'static>` and a runtime `ShapeId<'a>` with the same FQN
261    /// must compare equal and hash identically. This is what makes
262    /// cross-lifetime lookup against a `'static`-keyed table sound.
263    #[test]
264    fn equality_across_lifetimes() {
265        let static_id: ShapeId<'static> = shape_id!("ns", "Foo");
266        let owned_fqn = String::from("ns#Foo");
267        let owned_ns = String::from("ns");
268        let owned_name = String::from("Foo");
269        let runtime_id: ShapeId<'_> = ShapeId::from_parts(&owned_fqn, &owned_ns, &owned_name);
270
271        assert_eq!(static_id, runtime_id);
272
273        // Hash agreement.
274        let mut h1 = std::collections::hash_map::DefaultHasher::new();
275        let mut h2 = std::collections::hash_map::DefaultHasher::new();
276        std::hash::Hash::hash(&static_id, &mut h1);
277        std::hash::Hash::hash(&runtime_id, &mut h2);
278        assert_eq!(
279            std::hash::Hasher::finish(&h1),
280            std::hash::Hasher::finish(&h2),
281        );
282    }
283
284    /// `Borrow<str>` plus FQN-only `Hash`/`Eq` lets us look up a
285    /// `HashMap<ShapeId<'static>, V>` with a bare `&str` key.
286    #[test]
287    fn hash_map_lookup_by_str_and_runtime_id() {
288        use std::collections::HashMap;
289
290        let static_id: ShapeId<'static> = shape_id!("ns", "Foo");
291        let mut map: HashMap<ShapeId<'static>, u32> = HashMap::new();
292        map.insert(static_id, 42);
293
294        // Lookup by `&str` (cross-lifetime).
295        assert_eq!(map.get("ns#Foo"), Some(&42));
296
297        // Lookup by a runtime-built `ShapeId<'_>` (cross-lifetime).
298        let owned_fqn = String::from("ns#Foo");
299        let owned_ns = String::from("ns");
300        let owned_name = String::from("Foo");
301        let runtime_id: ShapeId<'_> = ShapeId::from_parts(&owned_fqn, &owned_ns, &owned_name);
302        assert_eq!(map.get(runtime_id.as_str()), Some(&42));
303
304        // Negative case.
305        assert_eq!(map.get("ns#Bar"), None);
306    }
307}