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}