aws_smithy_eventstream/
smithy.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::error::{Error, ErrorKind};
7use aws_smithy_types::event_stream::{Header, HeaderValue, Message};
8use aws_smithy_types::str_bytes::StrBytes;
9use aws_smithy_types::{Blob, DateTime};
10
11macro_rules! expect_shape_fn {
12    (fn $fn_name:ident[$val_typ:ident] -> $result_typ:ident { $val_name:ident -> $val_expr:expr }) => {
13        #[doc = "Expects that `header` is a `"]
14        #[doc = stringify!($result_typ)]
15        #[doc = "`."]
16        pub fn $fn_name(header: &Header) -> Result<$result_typ, Error> {
17            match header.value() {
18                HeaderValue::$val_typ($val_name) => Ok($val_expr),
19                _ => Err(ErrorKind::Unmarshalling(format!(
20                    "expected '{}' header value to be {}",
21                    header.name().as_str(),
22                    stringify!($val_typ)
23                ))
24                .into()),
25            }
26        }
27    };
28}
29
30expect_shape_fn!(fn expect_bool[Bool] -> bool { value -> *value });
31expect_shape_fn!(fn expect_byte[Byte] -> i8 { value -> *value });
32expect_shape_fn!(fn expect_int16[Int16] -> i16 { value -> *value });
33expect_shape_fn!(fn expect_int32[Int32] -> i32 { value -> *value });
34expect_shape_fn!(fn expect_int64[Int64] -> i64 { value -> *value });
35expect_shape_fn!(fn expect_byte_array[ByteArray] -> Blob { bytes -> Blob::new(bytes.as_ref()) });
36expect_shape_fn!(fn expect_string[String] -> String { value -> value.as_str().into() });
37expect_shape_fn!(fn expect_timestamp[Timestamp] -> DateTime { value -> *value });
38
39/// Structured header data from a [`Message`]
40#[derive(Debug)]
41pub struct ResponseHeaders<'a> {
42    /// Content Type of the message
43    ///
44    /// This can be a number of things depending on the protocol. For example, if the protocol is
45    /// AwsJson1, then this could be `application/json`, or `application/xml` for RestXml.
46    ///
47    /// It will be `application/octet-stream` if there is a Blob payload shape, and `text/plain` if
48    /// there is a String payload shape.
49    pub content_type: Option<&'a StrBytes>,
50
51    /// Message Type field
52    ///
53    /// This field is used to distinguish between events where the value is `event` and errors where
54    /// the value is `exception`
55    pub message_type: &'a StrBytes,
56
57    /// Smithy Type field
58    ///
59    /// This field is used to determine which of the possible union variants that this message represents
60    pub smithy_type: &'a StrBytes,
61}
62
63impl ResponseHeaders<'_> {
64    /// Content-Type for this message
65    pub fn content_type(&self) -> Option<&str> {
66        self.content_type.map(|ct| ct.as_str())
67    }
68}
69
70fn expect_header_str_value<'a>(
71    header: Option<&'a Header>,
72    name: &str,
73) -> Result<&'a StrBytes, Error> {
74    match header {
75        Some(header) => Ok(header.value().as_string().map_err(|value| {
76            Error::from(ErrorKind::Unmarshalling(format!(
77                "expected response {name} header to be string, received {value:?}"
78            )))
79        })?),
80        None => Err(ErrorKind::Unmarshalling(format!(
81            "expected response to include {name} header, but it was missing"
82        ))
83        .into()),
84    }
85}
86
87/// Parse headers from [`Message`]
88///
89/// `:content-type`, `:message-type`, `:event-type`, and `:exception-type` headers will be parsed.
90/// If any headers are invalid or missing, an error will be returned.
91pub fn parse_response_headers(message: &Message) -> Result<ResponseHeaders<'_>, Error> {
92    let (mut content_type, mut message_type, mut event_type, mut exception_type) =
93        (None, None, None, None);
94    for header in message.headers() {
95        match header.name().as_str() {
96            ":content-type" => content_type = Some(header),
97            ":message-type" => message_type = Some(header),
98            ":event-type" => event_type = Some(header),
99            ":exception-type" => exception_type = Some(header),
100            _ => {}
101        }
102    }
103    let message_type = expect_header_str_value(message_type, ":message-type")?;
104    Ok(ResponseHeaders {
105        content_type: content_type
106            .map(|ct| expect_header_str_value(Some(ct), ":content-type"))
107            .transpose()?,
108        message_type,
109        smithy_type: if message_type.as_str() == "event" {
110            expect_header_str_value(event_type, ":event-type")?
111        } else if message_type.as_str() == "exception" {
112            expect_header_str_value(exception_type, ":exception-type")?
113        } else {
114            return Err(ErrorKind::Unmarshalling(format!(
115                "unrecognized `:message-type`: {}",
116                message_type.as_str()
117            ))
118            .into());
119        },
120    })
121}
122
123#[cfg(test)]
124mod tests {
125    use super::parse_response_headers;
126    use aws_smithy_types::event_stream::{Header, HeaderValue, Message};
127
128    #[test]
129    fn normal_message() {
130        let message = Message::new(&b"test"[..])
131            .add_header(Header::new(
132                ":event-type",
133                HeaderValue::String("Foo".into()),
134            ))
135            .add_header(Header::new(
136                ":content-type",
137                HeaderValue::String("application/json".into()),
138            ))
139            .add_header(Header::new(
140                ":message-type",
141                HeaderValue::String("event".into()),
142            ));
143        let parsed = parse_response_headers(&message).unwrap();
144        assert_eq!("Foo", parsed.smithy_type.as_str());
145        assert_eq!(Some("application/json"), parsed.content_type());
146        assert_eq!("event", parsed.message_type.as_str());
147    }
148
149    #[test]
150    fn error_message() {
151        let message = Message::new(&b"test"[..])
152            .add_header(Header::new(
153                ":exception-type",
154                HeaderValue::String("BadRequestException".into()),
155            ))
156            .add_header(Header::new(
157                ":content-type",
158                HeaderValue::String("application/json".into()),
159            ))
160            .add_header(Header::new(
161                ":message-type",
162                HeaderValue::String("exception".into()),
163            ));
164        let parsed = parse_response_headers(&message).unwrap();
165        assert_eq!("BadRequestException", parsed.smithy_type.as_str());
166        assert_eq!(Some("application/json"), parsed.content_type());
167        assert_eq!("exception", parsed.message_type.as_str());
168    }
169
170    #[test]
171    fn missing_exception_type() {
172        let message = Message::new(&b"test"[..])
173            .add_header(Header::new(
174                ":content-type",
175                HeaderValue::String("application/json".into()),
176            ))
177            .add_header(Header::new(
178                ":message-type",
179                HeaderValue::String("exception".into()),
180            ));
181        let error = parse_response_headers(&message).err().unwrap().to_string();
182        assert_eq!(
183            "failed to unmarshall message: expected response to include :exception-type \
184             header, but it was missing",
185            error
186        );
187    }
188
189    #[test]
190    fn missing_event_type() {
191        let message = Message::new(&b"test"[..])
192            .add_header(Header::new(
193                ":content-type",
194                HeaderValue::String("application/json".into()),
195            ))
196            .add_header(Header::new(
197                ":message-type",
198                HeaderValue::String("event".into()),
199            ));
200        let error = parse_response_headers(&message).err().unwrap().to_string();
201        assert_eq!(
202            "failed to unmarshall message: expected response to include :event-type \
203             header, but it was missing",
204            error
205        );
206    }
207
208    #[test]
209    fn missing_content_type() {
210        let message = Message::new(&b"test"[..])
211            .add_header(Header::new(
212                ":event-type",
213                HeaderValue::String("Foo".into()),
214            ))
215            .add_header(Header::new(
216                ":message-type",
217                HeaderValue::String("event".into()),
218            ));
219        let parsed = parse_response_headers(&message).ok().unwrap();
220        assert_eq!(None, parsed.content_type);
221        assert_eq!("Foo", parsed.smithy_type.as_str());
222        assert_eq!("event", parsed.message_type.as_str());
223    }
224
225    #[test]
226    fn content_type_wrong_type() {
227        let message = Message::new(&b"test"[..])
228            .add_header(Header::new(
229                ":event-type",
230                HeaderValue::String("Foo".into()),
231            ))
232            .add_header(Header::new(":content-type", HeaderValue::Int32(16)))
233            .add_header(Header::new(
234                ":message-type",
235                HeaderValue::String("event".into()),
236            ));
237        let error = parse_response_headers(&message).err().unwrap().to_string();
238        assert_eq!(
239            "failed to unmarshall message: expected response :content-type \
240             header to be string, received Int32(16)",
241            error
242        );
243    }
244}