aws_smithy_schema/schema/traits.rs
1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Typed runtime representations of Smithy serialization traits.
7//!
8//! These types allow type-safe downcasting from `&dyn Trait` via `as_any()`,
9//! enabling protocol implementations to read trait values without string-matching
10//! on Shape IDs.
11
12use crate::{ShapeId, Trait};
13use std::any::Any;
14
15macro_rules! annotation_trait {
16 ($(#[$meta:meta])* $name:ident, $ns:literal, $trait_name:literal) => {
17 $(#[$meta])*
18 #[derive(Debug, Clone)]
19 #[allow(dead_code)] // Used by generated schema code
20 pub struct $name;
21
22 impl $name {
23 /// The Shape ID for this trait.
24 pub const TRAIT_ID: ShapeId<'static> = crate::shape_id!($ns, $trait_name);
25 }
26
27 impl Trait for $name {
28 fn trait_id(&self) -> &ShapeId<'static> { &Self::TRAIT_ID }
29 fn as_any(&self) -> &dyn Any { self }
30 }
31 };
32}
33
34macro_rules! string_trait {
35 ($(#[$meta:meta])* $name:ident, $ns:literal, $trait_name:literal) => {
36 $(#[$meta])*
37 #[derive(Debug, Clone)]
38 #[allow(dead_code)] // Used by generated schema code
39 pub struct $name<'a> {
40 value: &'a str,
41 }
42
43 #[allow(dead_code)] // Used by generated schema code
44 impl<'a> $name<'a> {
45 /// The Shape ID for this trait.
46 pub const TRAIT_ID: ShapeId<'static> = crate::shape_id!($ns, $trait_name);
47
48 /// Creates a new instance.
49 ///
50 /// `value` is borrowed for `'a`. For codegen-emitted schemas this is
51 /// a string literal, so `'a` is `'static`; schemas materialized at
52 /// runtime borrow from storage the caller owns, such as an arena
53 /// holding the parsed model text.
54 pub const fn new(value: &'a str) -> Self {
55 Self { value }
56 }
57
58 /// Returns the trait value.
59 ///
60 /// Bound to the schema's data lifetime `'a`, not to `&self`, so the
61 /// value can be stored anywhere the schema's data is valid. For a
62 /// `'static` schema this is `&'static str`.
63 pub fn value(&self) -> &'a str {
64 self.value
65 }
66 }
67
68 // Implemented only for the `'static` instantiation: `Trait` requires the
69 // `Any` supertrait, which requires `Self: 'static`. Runtime-lifetime
70 // wrappers are still readable through the typed accessors on `Schema`;
71 // only the `dyn Trait` fallback map is `'static`-only. See
72 // `trait_type.rs` for the `Any` constraint.
73 impl Trait for $name<'static> {
74 fn trait_id(&self) -> &ShapeId<'static> { &Self::TRAIT_ID }
75 fn as_any(&self) -> &dyn Any { self }
76 }
77 };
78}
79
80// --- Serialization & Protocol traits ---
81
82string_trait!(
83 /// The `@jsonName` trait — overrides the JSON key for a member.
84 JsonNameTrait,
85 "smithy.api", "jsonName"
86);
87
88string_trait!(
89 /// The `@xmlName` trait — overrides the XML element name.
90 XmlNameTrait,
91 "smithy.api", "xmlName"
92);
93
94string_trait!(
95 /// The `@mediaType` trait — specifies the media type of a blob/string.
96 MediaTypeTrait,
97 "smithy.api", "mediaType"
98);
99
100annotation_trait!(
101 /// The `@xmlAttribute` trait — serializes a member as an XML attribute.
102 XmlAttributeTrait,
103 "smithy.api", "xmlAttribute"
104);
105
106annotation_trait!(
107 /// The `@xmlFlattened` trait — removes the wrapping element for lists/maps in XML.
108 XmlFlattenedTrait,
109 "smithy.api", "xmlFlattened"
110);
111
112// xmlNamespace is a structured trait carrying a URI and an optional prefix.
113// Hand-written rather than generated via the `string_trait!` macro because the
114// macro only models a single string value.
115
116/// The `@xmlNamespace` trait — adds an XML namespace declaration to elements
117/// generated for the targeted shape.
118///
119/// REST XML emits this as `xmlns="uri"` (no prefix) or `xmlns:prefix="uri"`
120/// on the start tag of the element to which the trait applies.
121#[derive(Debug, Clone)]
122#[allow(dead_code)] // Used by generated schema code
123pub struct XmlNamespaceTrait<'a> {
124 uri: &'a str,
125 prefix: Option<&'a str>,
126}
127
128#[allow(dead_code)] // Used by generated schema code
129impl<'a> XmlNamespaceTrait<'a> {
130 /// The Shape ID for this trait.
131 pub const TRAIT_ID: ShapeId<'static> = crate::shape_id!("smithy.api", "xmlNamespace");
132
133 /// Creates a new `XmlNamespaceTrait`.
134 ///
135 /// `uri` and `prefix` are borrowed for `'a`; see
136 /// [`JsonNameTrait::new`](crate::traits::JsonNameTrait::new) for what that
137 /// means for codegen-emitted versus runtime-materialized schemas.
138 pub const fn new(uri: &'a str, prefix: Option<&'a str>) -> Self {
139 Self { uri, prefix }
140 }
141
142 /// The namespace URI.
143 pub fn uri(&self) -> &'a str {
144 self.uri
145 }
146
147 /// The optional namespace prefix.
148 ///
149 /// When `Some(prefix)`, the namespace is declared as
150 /// `xmlns:prefix="uri"`. When `None`, it is declared as `xmlns="uri"`.
151 pub fn prefix(&self) -> Option<&'a str> {
152 self.prefix
153 }
154}
155
156impl Trait for XmlNamespaceTrait<'static> {
157 fn trait_id(&self) -> &ShapeId<'static> {
158 &Self::TRAIT_ID
159 }
160 fn as_any(&self) -> &dyn Any {
161 self
162 }
163}
164
165// --- Timestamp ---
166
167/// The `@timestampFormat` trait — specifies the serialization format for timestamps.
168#[derive(Debug, Clone, Copy)]
169#[allow(dead_code)] // Used by generated schema code
170pub struct TimestampFormatTrait {
171 format: TimestampFormat,
172}
173
174/// Timestamp serialization formats.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176#[non_exhaustive]
177pub enum TimestampFormat {
178 /// Epoch seconds (e.g. `1515531081.123`).
179 EpochSeconds,
180 /// RFC 3339 date-time (e.g. `2018-01-09T18:47:00Z`).
181 DateTime,
182 /// RFC 7231 HTTP date (e.g. `Tue, 09 Jan 2018 18:47:00 GMT`).
183 HttpDate,
184}
185
186#[allow(dead_code)] // Used by generated schema code
187impl TimestampFormatTrait {
188 /// The Shape ID for this trait.
189 pub const TRAIT_ID: ShapeId<'static> = crate::shape_id!("smithy.api", "timestampFormat");
190
191 /// Creates a new instance.
192 pub const fn new(format: TimestampFormat) -> Self {
193 Self { format }
194 }
195
196 /// Returns the timestamp format.
197 pub fn format(&self) -> TimestampFormat {
198 self.format
199 }
200}
201
202impl Trait for TimestampFormatTrait {
203 fn trait_id(&self) -> &ShapeId<'static> {
204 &Self::TRAIT_ID
205 }
206 fn as_any(&self) -> &dyn Any {
207 self
208 }
209}
210
211// --- HTTP binding traits ---
212
213// `@httpHeader` is hand-written rather than generated via `string_trait!`
214// because its value has two cases. Header names are inserted into `Headers`,
215// whose only allocation-free `AsHeaderComponent` impl is for `&'static str`
216// (`aws-smithy-runtime-api`'s `MaybeStatic = Cow<'static, str>`), so the binder
217// needs to recover a `'static` name from a schema whose lifetime is arbitrary.
218// Modelling that as a two-arm enum rather than pinning the type means relaxing
219// this trait later — to let runtime-materialized schemas borrow header names
220// from a model arena — is a purely additive change: add a `new_borrowed`
221// constructor and `value_static` starts returning `None`, with no signature
222// changed.
223//
224// Both arms are references, so the type has no drop glue and stays
225// `const fn`-constructible. That rules out `Cow<'static, str>` here: a
226// `const fn` body cannot run destructors, and generated schemas are
227// const-initialized statics.
228#[derive(Debug, Clone, Copy)]
229enum HeaderName<'a> {
230 /// A codegen-emitted literal, or a runtime name interned/leaked to `'static`.
231 Static(&'static str),
232 /// Reserved for runtime-materialized schemas that borrow the header name
233 /// from storage the caller owns. Not publicly constructible yet; adding a
234 /// constructor for it is backwards compatible.
235 #[allow(dead_code)]
236 Borrowed(&'a str),
237}
238
239/// The `@httpHeader` trait — binds a member to an HTTP header.
240#[derive(Debug, Clone)]
241#[allow(dead_code)] // Used by generated schema code
242pub struct HttpHeaderTrait<'a> {
243 value: HeaderName<'a>,
244}
245
246#[allow(dead_code)] // Used by generated schema code
247impl<'a> HttpHeaderTrait<'a> {
248 /// The Shape ID for this trait.
249 pub const TRAIT_ID: ShapeId<'static> = crate::shape_id!("smithy.api", "httpHeader");
250
251 /// Creates a new instance from a `'static` header name.
252 ///
253 /// Unlike the other wire-format traits this requires `'static`, so that
254 /// [`value_static`](Self::value_static) can hand the HTTP binder a name it
255 /// inserts into `Headers` without allocating. Schemas materialized at
256 /// runtime must intern or leak their header names; every other
257 /// wire-format trait accepts an arbitrary lifetime.
258 pub const fn new(value: &'static str) -> Self {
259 Self {
260 value: HeaderName::Static(value),
261 }
262 }
263
264 /// Returns the header name.
265 ///
266 /// Bound to the schema's data lifetime `'a`, matching the other trait
267 /// wrappers. Use [`value_static`](Self::value_static) when the name has to
268 /// outlive the schema, as it does for header insertion.
269 pub fn value(&self) -> &'a str {
270 match self.value {
271 HeaderName::Static(v) => v,
272 HeaderName::Borrowed(v) => v,
273 }
274 }
275
276 /// Returns the header name if it is `'static`.
277 ///
278 /// This is what `Headers::insert` needs for an allocation-free insert.
279 ///
280 /// Always `Some` today, because [`new`](Self::new) is the only constructor
281 /// and it requires `'static`. Callers must nonetheless handle `None` and
282 /// fall back to an owned string rather than unwrapping: relaxing this
283 /// trait to accept runtime-borrowed names is intended to be additive, and
284 /// at that point this returns `None` for arena-borrowed names.
285 pub fn value_static(&self) -> Option<&'static str> {
286 match self.value {
287 HeaderName::Static(v) => Some(v),
288 HeaderName::Borrowed(_) => None,
289 }
290 }
291}
292
293// Implemented only for the `'static` instantiation; see the note on the
294// `string_trait!` impl for why `Trait` requires `Self: 'static`.
295impl Trait for HttpHeaderTrait<'static> {
296 fn trait_id(&self) -> &ShapeId<'static> {
297 &Self::TRAIT_ID
298 }
299
300 fn as_any(&self) -> &dyn Any {
301 self
302 }
303}
304
305string_trait!(
306 /// The `@httpQuery` trait — binds a member to a query parameter.
307 HttpQueryTrait,
308 "smithy.api", "httpQuery"
309);
310
311string_trait!(
312 /// The `@httpPrefixHeaders` trait — binds a map to prefixed HTTP headers.
313 HttpPrefixHeadersTrait,
314 "smithy.api", "httpPrefixHeaders"
315);
316
317annotation_trait!(
318 /// The `@httpLabel` trait — binds a member to a URI label.
319 HttpLabelTrait,
320 "smithy.api", "httpLabel"
321);
322
323annotation_trait!(
324 /// The `@httpPayload` trait — binds a member to the HTTP body.
325 HttpPayloadTrait,
326 "smithy.api", "httpPayload"
327);
328
329annotation_trait!(
330 /// The `@httpQueryParams` trait — binds a map to query parameters.
331 HttpQueryParamsTrait,
332 "smithy.api", "httpQueryParams"
333);
334
335annotation_trait!(
336 /// The `@httpResponseCode` trait — binds a member to the HTTP status code.
337 HttpResponseCodeTrait,
338 "smithy.api", "httpResponseCode"
339);
340
341/// The `@http` trait — defines the HTTP method, URI pattern, and status code for an operation.
342///
343/// This is an operation-level trait that is included on the input schema for
344/// convenience, so that the protocol serializer can construct the correct
345/// request without needing a separate operation schema.
346///
347/// The URI pattern may contain `{label}` placeholders that are substituted
348/// at serialization time with percent-encoded values from `@httpLabel` members.
349#[derive(Debug, Clone)]
350pub struct HttpTrait<'a> {
351 method: &'a str,
352 uri: &'a str,
353 code: u16,
354}
355
356impl<'a> HttpTrait<'a> {
357 /// Creates a new `HttpTrait`. If `code` is `None`, defaults to `200`.
358 ///
359 /// `method` and `uri` are borrowed for `'a`. Without a non-`'static` `'a`, a
360 /// runtime-materialized schema has no method and no URI, so no request can
361 /// be constructed from it at all.
362 pub const fn new(method: &'a str, uri: &'a str, code: Option<u16>) -> Self {
363 Self {
364 method,
365 uri,
366 code: match code {
367 Some(c) => c,
368 None => 200,
369 },
370 }
371 }
372
373 /// The HTTP method (e.g., `"GET"`, `"POST"`, `"PUT"`).
374 pub fn method(&self) -> &'a str {
375 self.method
376 }
377
378 /// The URI pattern (e.g., `"/resource/{id}"`).
379 ///
380 /// May contain `{label}` placeholders that correspond to `@httpLabel` members.
381 /// The protocol serializer substitutes these with percent-encoded values
382 /// collected during member serialization.
383 pub fn uri(&self) -> &'a str {
384 self.uri
385 }
386
387 /// The HTTP status code for a successful response. Defaults to `200`.
388 pub fn code(&self) -> u16 {
389 self.code
390 }
391}
392
393// --- Streaming traits ---
394
395annotation_trait!(
396 /// The `@streaming` trait — marks a blob or union as streaming.
397 StreamingTrait,
398 "smithy.api", "streaming"
399);
400
401annotation_trait!(
402 /// The `@eventHeader` trait — binds a member to an event stream header.
403 EventHeaderTrait,
404 "smithy.api", "eventHeader"
405);
406
407annotation_trait!(
408 /// The `@eventPayload` trait — binds a member to an event stream payload.
409 EventPayloadTrait,
410 "smithy.api", "eventPayload"
411);
412
413// --- Documentation / behavior traits ---
414
415annotation_trait!(
416 /// The `@sensitive` trait — marks data as sensitive for logging redaction.
417 SensitiveTrait,
418 "smithy.api", "sensitive"
419);
420
421// --- Endpoint traits ---
422
423annotation_trait!(
424 /// The `@hostLabel` trait — binds a member to a host prefix label.
425 HostLabelTrait,
426 "smithy.api", "hostLabel"
427);
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn downcast_json_name() {
435 let t: Box<dyn Trait> = Box::new(JsonNameTrait::new("userName"));
436 assert_eq!(t.trait_id().as_str(), "smithy.api#jsonName");
437 // The downcast target is the `'static` instantiation: `Trait` requires
438 // `Any`, which is only implemented for `JsonNameTrait<'static>`.
439 let json_name = t.as_any().downcast_ref::<JsonNameTrait<'static>>().unwrap();
440 assert_eq!(json_name.value(), "userName");
441 }
442
443 #[test]
444 fn downcast_sensitive() {
445 let t: Box<dyn Trait> = Box::new(SensitiveTrait);
446 assert_eq!(t.trait_id().as_str(), "smithy.api#sensitive");
447 assert!(t.as_any().downcast_ref::<SensitiveTrait>().is_some());
448 }
449
450 /// The HTTP binder's allocation-free header insert depends on
451 /// `value_static()` being `Some` for every `@httpHeader` a schema can
452 /// currently hold. If a `Borrowed` constructor is ever added, this test
453 /// keeps passing and the binder falls back to an owned name rather than
454 /// silently losing the header.
455 #[test]
456 fn http_header_value_static_is_some() {
457 const H: HttpHeaderTrait<'static> = HttpHeaderTrait::new("x-amz-request-id");
458 assert_eq!(H.value(), "x-amz-request-id");
459 assert_eq!(H.value_static(), Some("x-amz-request-id"));
460 }
461
462 #[test]
463 fn timestamp_format_parsing() {
464 let t = TimestampFormatTrait::new(TimestampFormat::EpochSeconds);
465 assert_eq!(t.format(), TimestampFormat::EpochSeconds);
466 assert_eq!(t.trait_id().as_str(), "smithy.api#timestampFormat");
467 }
468}