aws_smithy_json/protocol/
aws_rest_json_1.rs1use crate::codec::{JsonCodec, JsonCodecSettings};
17use aws_smithy_schema::http_protocol::{
18 http_error_deserializer, http_output_deserializer, HttpBindingProtocol,
19};
20use aws_smithy_schema::{shape_id, Schema, ShapeId};
21use aws_smithy_types::config_bag::ConfigBag;
22
23static PROTOCOL_ID: ShapeId<'static> = shape_id!("aws.protocols", "restJson1");
24
25#[derive(Debug)]
31pub struct AwsRestJsonProtocol {
32 inner: HttpBindingProtocol<JsonCodec>,
33}
34
35impl AwsRestJsonProtocol {
36 pub fn new() -> Self {
38 let codec = JsonCodec::new(
39 JsonCodecSettings::builder()
40 .use_json_name(true)
41 .default_timestamp_format(aws_smithy_types::date_time::Format::EpochSeconds)
42 .protocol_id(PROTOCOL_ID.clone())
43 .build(),
44 );
45 Self {
46 inner: HttpBindingProtocol::new(PROTOCOL_ID.clone(), codec, "application/json"),
47 }
48 }
49
50 pub fn with_default_namespace(self, namespace: impl Into<String>) -> Self {
69 let new_settings = self
70 .inner
71 .codec()
72 .settings()
73 .to_builder()
74 .default_namespace(namespace)
75 .build();
76 let new_codec = JsonCodec::new(new_settings);
77 Self {
78 inner: self.inner.with_codec(new_codec),
79 }
80 }
81
82 pub fn inner(&self) -> &HttpBindingProtocol<JsonCodec> {
84 &self.inner
85 }
86}
87
88impl Default for AwsRestJsonProtocol {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94impl aws_smithy_schema::protocol::ClientProtocolInner for AwsRestJsonProtocol {
95 type Request = aws_smithy_runtime_api::http::Request;
96 type Response = aws_smithy_runtime_api::http::Response;
97
98 fn protocol_id(&self) -> &ShapeId<'static> {
99 self.inner.protocol_id()
100 }
101
102 fn serialize_request(
103 &self,
104 input: &dyn aws_smithy_schema::serde::SerializableStruct,
105 input_schema: &Schema<'_>,
106 endpoint: &str,
107 cfg: &ConfigBag,
108 ) -> Result<aws_smithy_runtime_api::http::Request, aws_smithy_schema::serde::SerdeError> {
109 self.inner
110 .serialize_request(input, input_schema, endpoint, cfg)
111 }
112
113 fn deserialize_response<'a>(
114 &self,
115 response: &'a aws_smithy_runtime_api::http::Response,
116 output_schema: &Schema<'_>,
117 cfg: &'a ConfigBag,
118 ) -> Result<
119 Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
120 aws_smithy_schema::serde::SerdeError,
121 > {
122 if let Some(codec) = crate::protocol::codec_with_bag_namespace(self.inner.codec(), cfg) {
126 let body = response.body().bytes().unwrap_or(&[]);
135 return Ok(http_output_deserializer(
136 aws_smithy_schema::codec::Codec::create_deserializer(&codec, body),
137 response,
138 output_schema,
139 cfg,
140 ));
141 }
142 self.inner
143 .deserialize_response(response, output_schema, cfg)
144 }
145
146 fn deserialize_error_response<'a>(
166 &self,
167 response: &'a aws_smithy_runtime_api::http::Response,
168 cfg: &'a ConfigBag,
169 ) -> Result<
170 Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
171 aws_smithy_schema::serde::SerdeError,
172 > {
173 if let Some(codec) = crate::protocol::codec_with_bag_namespace(self.inner.codec(), cfg) {
174 let body = response.body().bytes().unwrap_or(&[]);
175 return Ok(http_error_deserializer(
176 aws_smithy_schema::codec::Codec::create_deserializer(&codec, body),
177 response,
178 cfg,
179 ));
180 }
181 self.inner.deserialize_error_response(response, cfg)
182 }
183
184 fn parse_error_metadata(
197 &self,
198 response: &aws_smithy_runtime_api::http::Response,
199 _cfg: &ConfigBag,
200 ) -> Result<aws_smithy_types::error::metadata::Builder, aws_smithy_schema::serde::SerdeError>
201 {
202 let body = response.body().bytes().unwrap_or(&[]);
203 crate::protocol::error::parse_error_envelope_metadata(body, response.headers())
204 }
205
206 fn payload_codec(&self) -> Option<&dyn aws_smithy_schema::codec::DynCodec> {
207 self.inner.payload_codec()
208 }
209
210 fn event_stream_media_type(&self) -> Option<&str> {
216 Some("application/json")
217 }
218
219 fn parse_event_stream_error_metadata(
226 &self,
227 payload: &[u8],
228 ) -> Result<aws_smithy_types::error::metadata::Builder, aws_smithy_schema::serde::SerdeError>
229 {
230 crate::protocol::error::parse_error_envelope_metadata(
231 payload,
232 &aws_smithy_runtime_api::http::Headers::new(),
233 )
234 }
235
236 fn update_endpoint(
237 &self,
238 request: &mut aws_smithy_runtime_api::http::Request,
239 endpoint: &aws_smithy_types::endpoint::Endpoint,
240 cfg: &ConfigBag,
241 ) -> Result<(), aws_smithy_schema::serde::SerdeError> {
242 self.inner.update_endpoint(request, endpoint, cfg)
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use aws_smithy_runtime_api::http::{Response, StatusCode};
250 use aws_smithy_schema::protocol::ClientProtocolInner;
251 use aws_smithy_schema::serde::SerdeError;
252 use aws_smithy_types::body::SdkBody;
253
254 fn http_response(headers: &[(&str, &str)], body: &str) -> Response {
255 let mut response = Response::new(StatusCode::try_from(400).unwrap(), SdkBody::from(body));
256 for (name, value) in headers {
257 response
258 .headers_mut()
259 .insert(name.to_string(), value.to_string());
260 }
261 response
262 }
263
264 #[test]
265 fn protocol_id_is_rest_json_1() {
266 assert_eq!(
267 AwsRestJsonProtocol::new().protocol_id().as_str(),
268 "aws.protocols#restJson1"
269 );
270 }
271
272 #[test]
273 fn parse_error_metadata_extracts_code_and_message_from_body() {
274 let proto = AwsRestJsonProtocol::new();
275 let response = http_response(
276 &[],
277 r#"{"__type":"ValidationException","message":"bad input"}"#,
278 );
279 let cfg = ConfigBag::base();
280 let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
281 assert_eq!(meta.code(), Some("ValidationException"));
282 assert_eq!(meta.message(), Some("bad input"));
283 }
284
285 #[test]
286 fn parse_error_metadata_header_takes_priority() {
287 let proto = AwsRestJsonProtocol::new();
288 let response = http_response(
289 &[("x-amzn-errortype", "FromHeader")],
290 r#"{"__type":"FromBody"}"#,
291 );
292 let cfg = ConfigBag::base();
293 let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
294 assert_eq!(meta.code(), Some("FromHeader"));
295 }
296
297 #[test]
298 fn parse_error_metadata_sanitizes_namespaced_code() {
299 let proto = AwsRestJsonProtocol::new();
300 let response = http_response(&[("x-amzn-errortype", "ns#FooError:http://example/")], "");
301 let cfg = ConfigBag::base();
302 let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
303 assert_eq!(meta.code(), Some("FooError"));
304 }
305
306 #[test]
307 fn parse_error_metadata_empty_body_returns_empty_builder() {
308 let proto = AwsRestJsonProtocol::new();
309 let response = http_response(&[], "");
310 let cfg = ConfigBag::base();
311 let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
312 assert!(meta.code().is_none());
313 assert!(meta.message().is_none());
314 }
315
316 #[test]
317 fn parse_error_metadata_malformed_body_returns_error() {
318 let proto = AwsRestJsonProtocol::new();
319 let response = http_response(&[], r#"{"__type":"Foo""#);
320 let cfg = ConfigBag::base();
321 let err = proto.parse_error_metadata(&response, &cfg).unwrap_err();
322 assert!(matches!(err, SerdeError::InvalidInput { .. }));
323 }
324
325 #[test]
326 fn with_default_namespace_propagates_to_codec_settings() {
327 let proto = AwsRestJsonProtocol::new().with_default_namespace("com.amazonaws.s3");
328 assert_eq!(
329 proto.inner.codec().settings().default_namespace(),
330 Some("com.amazonaws.s3"),
331 );
332 }
333
334 #[test]
335 fn with_default_namespace_preserves_other_settings() {
336 let proto = AwsRestJsonProtocol::new().with_default_namespace("com.example");
340 let settings = proto.inner.codec().settings();
341 assert_eq!(settings.default_namespace(), Some("com.example"));
342 assert_eq!(
343 settings.default_timestamp_format(),
344 aws_smithy_types::date_time::Format::EpochSeconds,
345 );
346 }
347
348 use aws_smithy_schema::protocol::ServiceShapeNamespace;
356 use aws_smithy_schema::ShapeType;
357 use aws_smithy_types::config_bag::Layer;
358
359 static NS_NAME: Schema<'static> =
360 Schema::new_member(shape_id!("test", "Out"), ShapeType::String, "name", 0)
361 .with_http_header("x-name");
362 static NS_STATUS: Schema<'static> =
363 Schema::new_member(shape_id!("test", "Out"), ShapeType::Integer, "status", 1)
364 .with_http_response_code();
365 static NS_BODY: Schema<'static> =
366 Schema::new_member(shape_id!("test", "Out"), ShapeType::String, "note", 2);
367 static NS_OUT: Schema<'static> = Schema::new_struct(
368 shape_id!("test", "Out"),
369 ShapeType::Structure,
370 &[&NS_NAME, &NS_STATUS, &NS_BODY],
371 );
372
373 fn bag_with_namespace() -> ConfigBag {
376 let mut layer = Layer::new("test");
377 layer.store_put(ServiceShapeNamespace::new("com.amazonaws.dynamodb"));
378 ConfigBag::of_layers(vec![layer])
379 }
380
381 fn read_bound(
382 mut deser: Box<dyn aws_smithy_schema::serde::ShapeDeserializer + '_>,
383 schema: &Schema<'_>,
384 ) -> (Option<String>, Option<i32>) {
385 let mut name = None;
386 let mut status = None;
387 deser
388 .read_struct(schema, &mut |member, d| {
389 match member.member_name() {
390 Some("name") => name = Some(d.read_string(member)?),
391 Some("status") => status = Some(d.read_integer(member)?),
392 _ => {
396 let _ = d.read_string(member)?;
397 }
398 }
399 Ok(())
400 })
401 .expect("deserialization succeeds");
402 (name, status)
403 }
404
405 #[test]
406 fn the_namespace_branch_still_reads_modeled_headers_and_status() {
407 let proto = AwsRestJsonProtocol::new();
408 let cfg = bag_with_namespace();
409 assert!(
410 crate::protocol::codec_with_bag_namespace(proto.inner().codec(), &cfg).is_some(),
411 "precondition: this response must take the namespace branch"
412 );
413 let response = http_response(&[("x-name", "widget")], r#"{"note":"n"}"#);
414 let (name, status) = read_bound(
415 proto
416 .deserialize_response(&response, &NS_OUT, &cfg)
417 .unwrap(),
418 &NS_OUT,
419 );
420 assert_eq!(name.as_deref(), Some("widget"));
421 assert_eq!(status, Some(400));
422 }
423
424 #[test]
425 fn the_error_path_reads_modeled_headers_and_status_too() {
426 let proto = AwsRestJsonProtocol::new();
427 let cfg = bag_with_namespace();
428 let response = http_response(&[("x-name", "missing")], r#"{"note":"n"}"#);
429 let (name, status) = read_bound(
430 proto.deserialize_error_response(&response, &cfg).unwrap(),
431 &NS_OUT,
432 );
433 assert_eq!(name.as_deref(), Some("missing"));
434 assert_eq!(status, Some(400));
435 }
436
437 #[test]
438 fn the_error_path_reads_bindings_without_the_namespace_branch_too() {
439 let proto = AwsRestJsonProtocol::new();
442 let cfg = ConfigBag::base();
443 assert!(
444 crate::protocol::codec_with_bag_namespace(proto.inner().codec(), &cfg).is_none(),
445 "precondition: this response must delegate to the inner protocol"
446 );
447 let response = http_response(&[("x-name", "missing")], r#"{"note":"n"}"#);
448 let (name, status) = read_bound(
449 proto.deserialize_error_response(&response, &cfg).unwrap(),
450 &NS_OUT,
451 );
452 assert_eq!(name.as_deref(), Some("missing"));
453 assert_eq!(status, Some(400));
454 }
455}