Skip to main content

aws_smithy_http_server/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 HTTP trait values stored in a schema's generic trait map.
7
8use aws_smithy_schema::{Schema, ShapeId, Trait};
9use std::any::Any;
10
11/// XML-specific information attached to a structured HTTP payload member.
12///
13/// The payload schema retains the target's metadata and members, with its XML
14/// root name overridden by the payload member. Other protocols use the ordinary
15/// target schema. Each generated payload schema owns its codec cache.
16#[derive(Debug, Clone, Copy)]
17pub struct XmlMetadata {
18    payload_schema: &'static Schema<'static>,
19}
20
21impl XmlMetadata {
22    /// The private server metadata trait's Shape ID.
23    pub const TRAIT_ID: ShapeId<'static> = aws_smithy_schema::shape_id!("smithy.rust.server.xml", "metadata");
24
25    /// Creates metadata referring to a prebuilt XML payload schema.
26    pub const fn new(payload_schema: &'static Schema<'static>) -> Self {
27        Self { payload_schema }
28    }
29
30    /// Returns the target schema with the payload member's XML root name.
31    pub const fn payload_schema(&self) -> &'static Schema<'static> {
32        self.payload_schema
33    }
34
35    /// Reads XML metadata from a payload member's trait map.
36    pub fn from_schema<'s>(schema: &'s Schema<'_>) -> Option<&'s Self> {
37        schema.traits()?.get(&Self::TRAIT_ID)?.as_any().downcast_ref()
38    }
39}
40
41impl Trait for XmlMetadata {
42    fn trait_id(&self) -> &ShapeId<'static> {
43        &Self::TRAIT_ID
44    }
45
46    fn as_any(&self) -> &dyn Any {
47        self
48    }
49}
50
51/// The `smithy.api#httpError` trait on an error structure.
52#[derive(Debug, Clone, Copy)]
53pub struct HttpErrorTrait {
54    code: u16,
55}
56
57impl HttpErrorTrait {
58    /// The Shape ID for this trait.
59    pub const TRAIT_ID: ShapeId<'static> = aws_smithy_schema::shape_id!("smithy.api", "httpError");
60
61    /// Creates an HTTP error trait with the modeled status code.
62    pub const fn new(code: u16) -> Self {
63        Self { code }
64    }
65
66    /// Returns the modeled HTTP error status code.
67    pub const fn code(&self) -> u16 {
68        self.code
69    }
70
71    /// Reads this typed trait from a schema's fallback map.
72    ///
73    /// Initializes the lazy trait map on first access. Returns `None` if the
74    /// schema has no map or the entry is absent or has a different Rust type.
75    pub fn from_schema<'s>(schema: &'s Schema<'_>) -> Option<&'s Self> {
76        schema.traits()?.get(&Self::TRAIT_ID)?.as_any().downcast_ref()
77    }
78}
79
80impl Trait for HttpErrorTrait {
81    fn trait_id(&self) -> &ShapeId<'static> {
82        &Self::TRAIT_ID
83    }
84
85    fn as_any(&self) -> &dyn Any {
86        self
87    }
88}
89
90/// The `HttpConfiguration` members every built-in protocol trait shares (Smithy's
91/// `aws.protocols#HttpConfiguration` mixin).
92///
93/// Recorded as written in the model; nothing in the server acts on them yet. HTTP version
94/// negotiation comes from the server's own configuration.
95#[derive(Debug, Clone, Default, PartialEq, Eq)]
96pub struct HttpConfiguration {
97    http: Option<Vec<String>>,
98    event_stream_http: Option<Vec<String>>,
99}
100
101impl HttpConfiguration {
102    /// Sets the `http` member.
103    ///
104    /// Public so protocol traits defined outside this crate (Smithy protocols declare these
105    /// members per protocol, by the `aws.protocols#HttpConfiguration` mixin convention) can embed
106    /// this struct the way the built-in protocol traits do.
107    pub fn with_http(mut self, versions: Vec<String>) -> Self {
108        self.http = Some(versions);
109        self
110    }
111
112    /// Sets the `eventStreamHttp` member.
113    pub fn with_event_stream_http(mut self, versions: Vec<String>) -> Self {
114        self.event_stream_http = Some(versions);
115        self
116    }
117
118    /// The priority-ordered HTTP versions the service supports, if the model lists them.
119    pub fn http(&self) -> Option<&[String]> {
120        self.http.as_deref()
121    }
122
123    /// The priority-ordered HTTP versions event streams require, if the model lists them.
124    /// When unset, Smithy defines the value as [`http`](Self::http).
125    pub fn event_stream_http(&self) -> Option<&[String]> {
126        self.event_stream_http.as_deref()
127    }
128}
129
130macro_rules! protocol_trait {
131    ($(#[$doc:meta])* $name:ident, $namespace:literal, $trait_name:literal $(, $field:ident: $ty:ty, $setter:ident, $getter:ident, $field_doc:literal)?) => {
132        $(#[$doc])*
133        #[derive(Debug, Clone, Default, PartialEq, Eq)]
134        pub struct $name {
135            http_configuration: HttpConfiguration,
136            $($field: $ty,)?
137        }
138
139        impl $name {
140            /// The Shape ID for this trait.
141            pub const TRAIT_ID: ShapeId<'static> = aws_smithy_schema::shape_id!($namespace, $trait_name);
142
143            /// Creates the trait with no members set.
144            pub fn new() -> Self {
145                Self::default()
146            }
147
148            /// Sets the `http` member.
149            pub fn with_http(mut self, versions: Vec<String>) -> Self {
150                self.http_configuration.http = Some(versions);
151                self
152            }
153
154            /// Sets the `eventStreamHttp` member.
155            pub fn with_event_stream_http(mut self, versions: Vec<String>) -> Self {
156                self.http_configuration.event_stream_http = Some(versions);
157                self
158            }
159
160            /// Returns the `http` and `eventStreamHttp` members.
161            pub fn http_configuration(&self) -> &HttpConfiguration {
162                &self.http_configuration
163            }
164
165            $(
166                #[doc = concat!("Sets the `", $field_doc, "` member.")]
167                pub fn $setter(mut self, value: $ty) -> Self {
168                    self.$field = value;
169                    self
170                }
171
172                #[doc = concat!("Returns the `", $field_doc, "` member.")]
173                pub fn $getter(&self) -> $ty {
174                    self.$field
175                }
176            )?
177
178            /// Reads this trait from the traits recorded on a service schema.
179            pub fn from_service<'s>(service: &'s crate::schema::ServiceSchema<'_>) -> Option<&'s Self> {
180                service.traits()?.get(&Self::TRAIT_ID)?.as_any().downcast_ref()
181            }
182        }
183
184        impl Trait for $name {
185            fn trait_id(&self) -> &ShapeId<'static> {
186                &Self::TRAIT_ID
187            }
188
189            fn as_any(&self) -> &dyn Any {
190                self
191            }
192        }
193    };
194}
195
196protocol_trait!(
197    /// The `aws.protocols#awsJson1_0` protocol trait on a service.
198    AwsJson1_0Trait, "aws.protocols", "awsJson1_0"
199);
200protocol_trait!(
201    /// The `aws.protocols#awsJson1_1` protocol trait on a service.
202    AwsJson1_1Trait, "aws.protocols", "awsJson1_1"
203);
204protocol_trait!(
205    /// The `aws.protocols#restJson1` protocol trait on a service.
206    RestJson1Trait, "aws.protocols", "restJson1"
207);
208protocol_trait!(
209    /// The `aws.protocols#restXml` protocol trait on a service.
210    ///
211    /// `noErrorWrapping` (deprecated in Smithy) drops the `<ErrorResponse>` envelope around
212    /// errors: a received exception frame is then rooted at `<Error>`.
213    RestXmlTrait, "aws.protocols", "restXml",
214    no_error_wrapping: bool, with_no_error_wrapping, no_error_wrapping, "noErrorWrapping"
215);
216protocol_trait!(
217    /// The `smithy.protocols#rpcv2Cbor` protocol trait on a service.
218    Rpcv2CborTrait, "smithy.protocols", "rpcv2Cbor"
219);
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use aws_smithy_schema::{shape_id, ShapeType, StringTrait, TraitMap};
225    use std::sync::LazyLock;
226
227    static ERROR_TRAITS: LazyLock<TraitMap> = LazyLock::new(|| {
228        let mut map = TraitMap::new();
229        map.insert(Box::new(StringTrait::new(shape_id!("smithy.api", "error"), "client")));
230        map.insert(Box::new(HttpErrorTrait::new(404)));
231        map
232    });
233
234    static ERROR: Schema<'static> =
235        Schema::new_struct(shape_id!("example", "Missing"), ShapeType::Structure, &[]).with_traits(&ERROR_TRAITS);
236
237    static SERVICE_TRAITS: LazyLock<TraitMap> = LazyLock::new(|| {
238        let mut map = TraitMap::new();
239        map.insert(Box::new(
240            RestXmlTrait::new()
241                .with_http(vec!["h2".into(), "http/1.1".into()])
242                .with_event_stream_http(vec!["h2".into()])
243                .with_no_error_wrapping(true),
244        ));
245        map.insert(Box::new(RestJson1Trait::new()));
246        map
247    });
248
249    static SERVICE: crate::schema::ServiceSchema<'static> = crate::schema::ServiceSchema::new(
250        shape_id!("example", "Service"),
251        None,
252        &[RestXmlTrait::TRAIT_ID, RestJson1Trait::TRAIT_ID],
253        &[],
254    )
255    .with_traits(&SERVICE_TRAITS);
256
257    #[test]
258    fn protocol_traits_are_read_from_the_service_schema() {
259        let rest_xml = RestXmlTrait::from_service(&SERVICE).unwrap();
260        assert!(rest_xml.no_error_wrapping());
261        assert_eq!(
262            rest_xml.http_configuration().http(),
263            Some(&["h2".to_owned(), "http/1.1".to_owned()][..])
264        );
265        assert_eq!(
266            rest_xml.http_configuration().event_stream_http(),
267            Some(&["h2".to_owned()][..])
268        );
269        assert_eq!(rest_xml.trait_id().as_str(), "aws.protocols#restXml");
270
271        // Members left unset in the model stay unset.
272        let rest_json = RestJson1Trait::from_service(&SERVICE).unwrap();
273        assert_eq!(rest_json.http_configuration(), &HttpConfiguration::default());
274        assert_eq!(rest_json.http_configuration().http(), None);
275
276        // Protocols the service does not declare, and services without traits, have none.
277        assert!(Rpcv2CborTrait::from_service(&SERVICE).is_none());
278        static BARE: crate::schema::ServiceSchema<'static> =
279            crate::schema::ServiceSchema::new(shape_id!("example", "Bare"), None, &[], &[]);
280        assert!(RestXmlTrait::from_service(&BARE).is_none());
281        assert_eq!(AwsJson1_0Trait::TRAIT_ID.as_str(), "aws.protocols#awsJson1_0");
282        assert_eq!(AwsJson1_1Trait::TRAIT_ID.as_str(), "aws.protocols#awsJson1_1");
283        assert_eq!(Rpcv2CborTrait::TRAIT_ID.as_str(), "smithy.protocols#rpcv2Cbor");
284    }
285
286    #[test]
287    fn typed_http_error_preserves_trait_identity() {
288        let schema = &ERROR;
289        assert!(schema.http().is_none());
290        let traits = schema.traits().unwrap();
291        assert!(traits.contains_fqn("smithy.api#error"));
292        assert!(traits.contains_fqn("smithy.api#httpError"));
293        assert_eq!(traits.len(), 2);
294        let http_error = HttpErrorTrait::from_schema(schema).unwrap();
295        assert_eq!(http_error.code(), 404);
296        assert_eq!(http_error.trait_id().as_str(), "smithy.api#httpError");
297    }
298
299    #[test]
300    fn absent_or_differently_typed_entries_return_none() {
301        let plain = Schema::new(shape_id!("example", "Plain"), ShapeType::String);
302        assert!(HttpErrorTrait::from_schema(&plain).is_none());
303        let without_http_error: LazyLock<TraitMap> = LazyLock::new(|| {
304            let mut map = TraitMap::new();
305            map.insert(Box::new(StringTrait::new(shape_id!("smithy.api", "error"), "server")));
306            map
307        });
308        assert!(HttpErrorTrait::from_schema(
309            &Schema::new(shape_id!("example", "Plain"), ShapeType::String).with_traits(&without_http_error)
310        )
311        .is_none());
312        let differently_typed: LazyLock<TraitMap> = LazyLock::new(|| {
313            let mut map = TraitMap::new();
314            map.insert(Box::new(StringTrait::new(HttpErrorTrait::TRAIT_ID, "404")));
315            map
316        });
317        assert!(HttpErrorTrait::from_schema(&plain.with_traits(&differently_typed)).is_none());
318    }
319}