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 = crate::shape_id!($ns, $trait_name);
25        }
26
27        impl Trait for $name {
28            fn trait_id(&self) -> &ShapeId { &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 {
40            value: &'static str,
41        }
42
43        #[allow(dead_code)] // Used by generated schema code
44        impl $name {
45            /// The Shape ID for this trait.
46            pub const TRAIT_ID: ShapeId = crate::shape_id!($ns, $trait_name);
47
48            /// Creates a new instance.
49            pub const fn new(value: &'static str) -> Self {
50                Self { value }
51            }
52
53            /// Returns the trait value.
54            pub fn value(&self) -> &'static str {
55                self.value
56            }
57        }
58
59        impl Trait for $name {
60            fn trait_id(&self) -> &ShapeId { &Self::TRAIT_ID }
61            fn as_any(&self) -> &dyn Any { self }
62        }
63    };
64}
65
66// --- Serialization & Protocol traits ---
67
68string_trait!(
69    /// The `@jsonName` trait — overrides the JSON key for a member.
70    JsonNameTrait,
71    "smithy.api", "jsonName"
72);
73
74string_trait!(
75    /// The `@xmlName` trait — overrides the XML element name.
76    XmlNameTrait,
77    "smithy.api", "xmlName"
78);
79
80string_trait!(
81    /// The `@mediaType` trait — specifies the media type of a blob/string.
82    MediaTypeTrait,
83    "smithy.api", "mediaType"
84);
85
86annotation_trait!(
87    /// The `@xmlAttribute` trait — serializes a member as an XML attribute.
88    XmlAttributeTrait,
89    "smithy.api", "xmlAttribute"
90);
91
92annotation_trait!(
93    /// The `@xmlFlattened` trait — removes the wrapping element for lists/maps in XML.
94    XmlFlattenedTrait,
95    "smithy.api", "xmlFlattened"
96);
97
98// xmlNamespace is a structured trait carrying a URI and an optional prefix.
99// Hand-written rather than generated via the `string_trait!` macro because the
100// macro only models a single string value.
101
102/// The `@xmlNamespace` trait — adds an XML namespace declaration to elements
103/// generated for the targeted shape.
104///
105/// REST XML emits this as `xmlns="uri"` (no prefix) or `xmlns:prefix="uri"`
106/// on the start tag of the element to which the trait applies.
107#[derive(Debug, Clone)]
108#[allow(dead_code)] // Used by generated schema code
109pub struct XmlNamespaceTrait {
110    uri: &'static str,
111    prefix: Option<&'static str>,
112}
113
114#[allow(dead_code)] // Used by generated schema code
115impl XmlNamespaceTrait {
116    /// The Shape ID for this trait.
117    pub const TRAIT_ID: ShapeId = crate::shape_id!("smithy.api", "xmlNamespace");
118
119    /// Creates a new `XmlNamespaceTrait`.
120    pub const fn new(uri: &'static str, prefix: Option<&'static str>) -> Self {
121        Self { uri, prefix }
122    }
123
124    /// The namespace URI.
125    pub fn uri(&self) -> &'static str {
126        self.uri
127    }
128
129    /// The optional namespace prefix.
130    ///
131    /// When `Some(prefix)`, the namespace is declared as
132    /// `xmlns:prefix="uri"`. When `None`, it is declared as `xmlns="uri"`.
133    pub fn prefix(&self) -> Option<&'static str> {
134        self.prefix
135    }
136}
137
138impl Trait for XmlNamespaceTrait {
139    fn trait_id(&self) -> &ShapeId {
140        &Self::TRAIT_ID
141    }
142    fn as_any(&self) -> &dyn Any {
143        self
144    }
145}
146
147// --- Timestamp ---
148
149/// The `@timestampFormat` trait — specifies the serialization format for timestamps.
150#[derive(Debug, Clone, Copy)]
151#[allow(dead_code)] // Used by generated schema code
152pub struct TimestampFormatTrait {
153    format: TimestampFormat,
154}
155
156/// Timestamp serialization formats.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum TimestampFormat {
159    /// Epoch seconds (e.g. `1515531081.123`).
160    EpochSeconds,
161    /// RFC 3339 date-time (e.g. `2018-01-09T18:47:00Z`).
162    DateTime,
163    /// RFC 7231 HTTP date (e.g. `Tue, 09 Jan 2018 18:47:00 GMT`).
164    HttpDate,
165}
166
167#[allow(dead_code)] // Used by generated schema code
168impl TimestampFormatTrait {
169    /// The Shape ID for this trait.
170    pub const TRAIT_ID: ShapeId = crate::shape_id!("smithy.api", "timestampFormat");
171
172    /// Creates a new instance.
173    pub const fn new(format: TimestampFormat) -> Self {
174        Self { format }
175    }
176
177    /// Returns the timestamp format.
178    pub fn format(&self) -> TimestampFormat {
179        self.format
180    }
181}
182
183impl Trait for TimestampFormatTrait {
184    fn trait_id(&self) -> &ShapeId {
185        &Self::TRAIT_ID
186    }
187    fn as_any(&self) -> &dyn Any {
188        self
189    }
190}
191
192// --- HTTP binding traits ---
193
194string_trait!(
195    /// The `@httpHeader` trait — binds a member to an HTTP header.
196    HttpHeaderTrait,
197    "smithy.api", "httpHeader"
198);
199
200string_trait!(
201    /// The `@httpQuery` trait — binds a member to a query parameter.
202    HttpQueryTrait,
203    "smithy.api", "httpQuery"
204);
205
206string_trait!(
207    /// The `@httpPrefixHeaders` trait — binds a map to prefixed HTTP headers.
208    HttpPrefixHeadersTrait,
209    "smithy.api", "httpPrefixHeaders"
210);
211
212annotation_trait!(
213    /// The `@httpLabel` trait — binds a member to a URI label.
214    HttpLabelTrait,
215    "smithy.api", "httpLabel"
216);
217
218annotation_trait!(
219    /// The `@httpPayload` trait — binds a member to the HTTP body.
220    HttpPayloadTrait,
221    "smithy.api", "httpPayload"
222);
223
224annotation_trait!(
225    /// The `@httpQueryParams` trait — binds a map to query parameters.
226    HttpQueryParamsTrait,
227    "smithy.api", "httpQueryParams"
228);
229
230annotation_trait!(
231    /// The `@httpResponseCode` trait — binds a member to the HTTP status code.
232    HttpResponseCodeTrait,
233    "smithy.api", "httpResponseCode"
234);
235
236/// The `@http` trait — defines the HTTP method, URI pattern, and status code for an operation.
237///
238/// This is an operation-level trait that is included on the input schema for
239/// convenience, so that the protocol serializer can construct the correct
240/// request without needing a separate operation schema.
241///
242/// The URI pattern may contain `{label}` placeholders that are substituted
243/// at serialization time with percent-encoded values from `@httpLabel` members.
244#[derive(Debug, Clone)]
245pub struct HttpTrait {
246    method: &'static str,
247    uri: &'static str,
248    code: u16,
249}
250
251impl HttpTrait {
252    /// Creates a new `HttpTrait`. If `code` is `None`, defaults to `200`.
253    pub const fn new(method: &'static str, uri: &'static str, code: Option<u16>) -> Self {
254        Self {
255            method,
256            uri,
257            code: match code {
258                Some(c) => c,
259                None => 200,
260            },
261        }
262    }
263
264    /// The HTTP method (e.g., `"GET"`, `"POST"`, `"PUT"`).
265    pub fn method(&self) -> &str {
266        self.method
267    }
268
269    /// The URI pattern (e.g., `"/resource/{id}"`).
270    ///
271    /// May contain `{label}` placeholders that correspond to `@httpLabel` members.
272    /// The protocol serializer substitutes these with percent-encoded values
273    /// collected during member serialization.
274    pub fn uri(&self) -> &str {
275        self.uri
276    }
277
278    /// The HTTP status code for a successful response. Defaults to `200`.
279    pub fn code(&self) -> u16 {
280        self.code
281    }
282}
283
284// --- Streaming traits ---
285
286annotation_trait!(
287    /// The `@streaming` trait — marks a blob or union as streaming.
288    StreamingTrait,
289    "smithy.api", "streaming"
290);
291
292annotation_trait!(
293    /// The `@eventHeader` trait — binds a member to an event stream header.
294    EventHeaderTrait,
295    "smithy.api", "eventHeader"
296);
297
298annotation_trait!(
299    /// The `@eventPayload` trait — binds a member to an event stream payload.
300    EventPayloadTrait,
301    "smithy.api", "eventPayload"
302);
303
304// --- Documentation / behavior traits ---
305
306annotation_trait!(
307    /// The `@sensitive` trait — marks data as sensitive for logging redaction.
308    SensitiveTrait,
309    "smithy.api", "sensitive"
310);
311
312// --- Endpoint traits ---
313
314annotation_trait!(
315    /// The `@hostLabel` trait — binds a member to a host prefix label.
316    HostLabelTrait,
317    "smithy.api", "hostLabel"
318);
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn downcast_json_name() {
326        let t: Box<dyn Trait> = Box::new(JsonNameTrait::new("userName"));
327        assert_eq!(t.trait_id().as_str(), "smithy.api#jsonName");
328        let json_name = t.as_any().downcast_ref::<JsonNameTrait>().unwrap();
329        assert_eq!(json_name.value(), "userName");
330    }
331
332    #[test]
333    fn downcast_sensitive() {
334        let t: Box<dyn Trait> = Box::new(SensitiveTrait);
335        assert_eq!(t.trait_id().as_str(), "smithy.api#sensitive");
336        assert!(t.as_any().downcast_ref::<SensitiveTrait>().is_some());
337    }
338
339    #[test]
340    fn timestamp_format_parsing() {
341        let t = TimestampFormatTrait::new(TimestampFormat::EpochSeconds);
342        assert_eq!(t.format(), TimestampFormat::EpochSeconds);
343        assert_eq!(t.trait_id().as_str(), "smithy.api#timestampFormat");
344    }
345}