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
117    /// (e.g. from a wire-format `__type`). `fqn` must agree with
118    /// `namespace`/`shape_name` — the macro guarantees this via `concat!`.
119    ///
120    /// Prefer the [`shape_id!`] macro for literal ids so the parts cannot get
121    /// out of sync.
122    #[doc(hidden)]
123    pub const fn from_parts(fqn: &'a str, namespace: &'a str, shape_name: &'a str) -> Self {
124        Self {
125            fqn,
126            namespace,
127            shape_name,
128            member_name: None,
129        }
130    }
131
132    /// Creates a member `ShapeId` from its already-split component strings.
133    ///
134    /// See [`Self::from_parts`]; this is the member-shape variant.
135    ///
136    /// Prefer the [`shape_id!`] macro for literal ids so the parts cannot get
137    /// out of sync.
138    #[doc(hidden)]
139    pub const fn from_parts_with_member(
140        fqn: &'a str,
141        namespace: &'a str,
142        shape_name: &'a str,
143        member_name: &'a str,
144    ) -> Self {
145        Self {
146            fqn,
147            namespace,
148            shape_name,
149            member_name: Some(member_name),
150        }
151    }
152
153    /// Returns the fully qualified string representation (e.g. `"smithy.api#String"`).
154    ///
155    /// The return type is `&'a str` (the lifetime of the data, not the
156    /// receiver). Calling this on `ShapeId<'static>` yields `&'static str`,
157    /// preserving the codegen-side `'static` accessor guarantee.
158    pub const fn as_str(&self) -> &'a str {
159        self.fqn
160    }
161
162    /// Returns the namespace portion of the ShapeId.
163    ///
164    /// Lifetime is `'a` — see [`Self::as_str`] for rationale.
165    pub const fn namespace(&self) -> &'a str {
166        self.namespace
167    }
168
169    /// Returns the shape name portion of the ShapeId.
170    ///
171    /// Lifetime is `'a` — see [`Self::as_str`] for rationale.
172    pub const fn shape_name(&self) -> &'a str {
173        self.shape_name
174    }
175
176    /// Returns the member name if this is a member shape ID.
177    ///
178    /// Lifetime is `'a` — see [`Self::as_str`] for rationale.
179    pub fn member_name(&self) -> Option<&'a str> {
180        self.member_name
181    }
182}
183
184impl fmt::Display for ShapeId<'_> {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        f.write_str(self.fqn)
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_shape_id_macro() {
196        const ID: ShapeId<'static> = shape_id!("smithy.api", "String");
197        assert_eq!(ID.as_str(), "smithy.api#String");
198        assert_eq!(ID.namespace(), "smithy.api");
199        assert_eq!(ID.shape_name(), "String");
200        assert_eq!(ID.member_name(), None);
201    }
202
203    #[test]
204    fn test_shape_id_macro_with_member() {
205        const ID: ShapeId<'static> = shape_id!("com.example", "MyStruct", "field");
206        assert_eq!(ID.as_str(), "com.example#MyStruct$field");
207        assert_eq!(ID.namespace(), "com.example");
208        assert_eq!(ID.shape_name(), "MyStruct");
209        assert_eq!(ID.member_name(), Some("field"));
210    }
211
212    #[test]
213    fn test_display() {
214        let id = shape_id!("smithy.api", "String");
215        assert_eq!(format!("{id}"), "smithy.api#String");
216    }
217
218    #[test]
219    fn test_equality() {
220        let a = shape_id!("smithy.api", "String");
221        let b = shape_id!("smithy.api", "String");
222        assert_eq!(a, b);
223
224        let c = shape_id!("smithy.api", "String", "foo");
225        let d = shape_id!("smithy.api", "String", "foo");
226        assert_eq!(c, d);
227    }
228
229    /// Construct a `ShapeId<'a>` from non-`'static` data. Compile-tests
230    /// that the lifetime parameter does what we want — without this the
231    /// runtime-built ShapeId path would not be exercised by tests.
232    #[test]
233    fn test_runtime_lifetime() {
234        let fqn = String::from("ns#Foo");
235        let ns = String::from("ns");
236        let name = String::from("Foo");
237        let id: ShapeId<'_> = ShapeId::from_parts(&fqn, &ns, &name);
238        assert_eq!(id.as_str(), "ns#Foo");
239        assert_eq!(id.namespace(), "ns");
240        assert_eq!(id.shape_name(), "Foo");
241    }
242
243    /// `ShapeId<'static>` and a runtime `ShapeId<'a>` with the same FQN
244    /// must compare equal and hash identically. This is what makes
245    /// cross-lifetime lookup against a `'static`-keyed table sound.
246    #[test]
247    fn equality_across_lifetimes() {
248        let static_id: ShapeId<'static> = shape_id!("ns", "Foo");
249        let owned_fqn = String::from("ns#Foo");
250        let owned_ns = String::from("ns");
251        let owned_name = String::from("Foo");
252        let runtime_id: ShapeId<'_> = ShapeId::from_parts(&owned_fqn, &owned_ns, &owned_name);
253
254        assert_eq!(static_id, runtime_id);
255
256        // Hash agreement.
257        let mut h1 = std::collections::hash_map::DefaultHasher::new();
258        let mut h2 = std::collections::hash_map::DefaultHasher::new();
259        std::hash::Hash::hash(&static_id, &mut h1);
260        std::hash::Hash::hash(&runtime_id, &mut h2);
261        assert_eq!(
262            std::hash::Hasher::finish(&h1),
263            std::hash::Hasher::finish(&h2),
264        );
265    }
266
267    /// `Borrow<str>` plus FQN-only `Hash`/`Eq` lets us look up a
268    /// `HashMap<ShapeId<'static>, V>` with a bare `&str` key.
269    #[test]
270    fn hash_map_lookup_by_str_and_runtime_id() {
271        use std::collections::HashMap;
272
273        let static_id: ShapeId<'static> = shape_id!("ns", "Foo");
274        let mut map: HashMap<ShapeId<'static>, u32> = HashMap::new();
275        map.insert(static_id, 42);
276
277        // Lookup by `&str` (cross-lifetime).
278        assert_eq!(map.get("ns#Foo"), Some(&42));
279
280        // Lookup by a runtime-built `ShapeId<'_>` (cross-lifetime).
281        let owned_fqn = String::from("ns#Foo");
282        let owned_ns = String::from("ns");
283        let owned_name = String::from("Foo");
284        let runtime_id: ShapeId<'_> = ShapeId::from_parts(&owned_fqn, &owned_ns, &owned_name);
285        assert_eq!(map.get(runtime_id.as_str()), Some(&42));
286
287        // Negative case.
288        assert_eq!(map.get("ns#Bar"), None);
289    }
290}