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}