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}