Skip to main content

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}