1use crate::codec::{JsonCodec, JsonCodecSettings};
23use aws_smithy_runtime_api::client::orchestrator::Metadata;
24use aws_smithy_schema::http_protocol::HttpRpcProtocol;
25use aws_smithy_schema::protocol::ServiceShapeName;
26use aws_smithy_schema::{shape_id, Schema, ShapeId};
27use aws_smithy_types::config_bag::ConfigBag;
28
29#[derive(Debug)]
31pub struct AwsJsonRpcProtocol {
32 inner: HttpRpcProtocol<JsonCodec>,
33 target_prefix: Option<String>,
36}
37
38impl AwsJsonRpcProtocol {
39 pub fn aws_json_1_0() -> Self {
44 Self::new(
45 shape_id!("aws.protocols", "awsJson1_0"),
46 "application/x-amz-json-1.0",
47 )
48 }
49
50 pub fn aws_json_1_1() -> Self {
55 Self::new(
56 shape_id!("aws.protocols", "awsJson1_1"),
57 "application/x-amz-json-1.1",
58 )
59 }
60
61 fn new(protocol_id: ShapeId<'static>, content_type: &'static str) -> Self {
62 let codec = JsonCodec::new(
63 JsonCodecSettings::builder()
64 .use_json_name(false)
65 .default_timestamp_format(aws_smithy_types::date_time::Format::EpochSeconds)
66 .protocol_id(protocol_id.clone())
67 .build(),
68 );
69 Self {
70 inner: HttpRpcProtocol::new(protocol_id, codec, content_type),
71 target_prefix: None,
72 }
73 }
74
75 pub fn with_target_prefix(mut self, target_prefix: impl Into<String>) -> Self {
89 self.target_prefix = Some(target_prefix.into());
90 self
91 }
92
93 fn resolved_target_prefix<'a>(&'a self, cfg: &'a ConfigBag) -> Option<&'a str> {
97 self.target_prefix
98 .as_deref()
99 .or_else(|| cfg.load::<ServiceShapeName>().map(ServiceShapeName::as_str))
100 }
101
102 pub fn with_default_namespace(self, namespace: impl Into<String>) -> Self {
131 let new_settings = self
132 .inner
133 .codec()
134 .settings()
135 .to_builder()
136 .default_namespace(namespace)
137 .build();
138 let new_codec = JsonCodec::new(new_settings);
139 Self {
140 inner: self.inner.with_codec(new_codec),
141 target_prefix: self.target_prefix,
142 }
143 }
144}
145
146impl aws_smithy_schema::protocol::ClientProtocolInner for AwsJsonRpcProtocol {
147 type Request = aws_smithy_runtime_api::http::Request;
148 type Response = aws_smithy_runtime_api::http::Response;
149
150 fn protocol_id(&self) -> &ShapeId<'static> {
151 self.inner.protocol_id()
152 }
153
154 fn serialize_request(
166 &self,
167 input: &dyn aws_smithy_schema::serde::SerializableStruct,
168 input_schema: &Schema<'_>,
169 _endpoint: &str,
170 cfg: &ConfigBag,
171 ) -> Result<aws_smithy_runtime_api::http::Request, aws_smithy_schema::serde::SerdeError> {
172 let mut request = self
173 .inner
174 .serialize_request(input, input_schema, "/", cfg)?;
175 if let (Some(prefix), Some(metadata)) =
176 (self.resolved_target_prefix(cfg), cfg.load::<Metadata>())
177 {
178 request
179 .headers_mut()
180 .insert("X-Amz-Target", format!("{}.{}", prefix, metadata.name()));
181 }
182 Ok(request)
183 }
184
185 fn deserialize_response<'a>(
186 &self,
187 response: &'a aws_smithy_runtime_api::http::Response,
188 output_schema: &Schema<'_>,
189 cfg: &'a ConfigBag,
190 ) -> Result<
191 Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
192 aws_smithy_schema::serde::SerdeError,
193 > {
194 if let Some(codec) = crate::protocol::codec_with_bag_namespace(self.inner.codec(), cfg) {
198 let body = response.body().bytes().unwrap_or(&[]);
202 return Ok(Box::new(
203 aws_smithy_schema::codec::Codec::create_deserializer(&codec, body),
204 ));
205 }
206 self.inner
207 .deserialize_response(response, output_schema, cfg)
208 }
209
210 fn parse_error_metadata(
229 &self,
230 response: &aws_smithy_runtime_api::http::Response,
231 _cfg: &ConfigBag,
232 ) -> Result<aws_smithy_types::error::metadata::Builder, aws_smithy_schema::serde::SerdeError>
233 {
234 let body = response.body().bytes().unwrap_or(&[]);
235 crate::protocol::error::parse_error_envelope_metadata(body, response.headers())
236 }
237
238 fn payload_codec(&self) -> Option<&dyn aws_smithy_schema::codec::DynCodec> {
239 self.inner.payload_codec()
240 }
241
242 fn event_stream_media_type(&self) -> Option<&str> {
248 Some("application/json")
249 }
250
251 fn parse_event_stream_error_metadata(
257 &self,
258 payload: &[u8],
259 ) -> Result<aws_smithy_types::error::metadata::Builder, aws_smithy_schema::serde::SerdeError>
260 {
261 crate::protocol::error::parse_error_envelope_metadata(
262 payload,
263 &aws_smithy_runtime_api::http::Headers::new(),
264 )
265 }
266
267 fn update_endpoint(
268 &self,
269 request: &mut aws_smithy_runtime_api::http::Request,
270 endpoint: &aws_smithy_types::endpoint::Endpoint,
271 cfg: &ConfigBag,
272 ) -> Result<(), aws_smithy_schema::serde::SerdeError> {
273 self.inner.update_endpoint(request, endpoint, cfg)
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use aws_smithy_schema::protocol::ClientProtocolInner;
281 use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeSerializer};
282 use aws_smithy_schema::ShapeType;
283 use aws_smithy_types::config_bag::Layer;
284
285 struct EmptyStruct;
286 impl SerializableStruct for EmptyStruct {
287 fn schema(&self) -> &aws_smithy_schema::Schema<'_> {
288 &TEST_SCHEMA
289 }
290
291 fn serialize_members(&self, _: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
292 Ok(())
293 }
294 }
295
296 static TEST_SCHEMA: aws_smithy_schema::Schema =
297 aws_smithy_schema::Schema::new(shape_id!("test", "Input"), ShapeType::Structure);
298
299 fn cfg_with_metadata(service: &str, operation: &str) -> ConfigBag {
300 let mut layer = Layer::new("test");
301 layer.store_put(Metadata::new(operation.to_string(), service.to_string()));
302 ConfigBag::of_layers(vec![layer])
303 }
304
305 fn cfg_with_service_shape_name(shape_name: &'static str, operation: &str) -> ConfigBag {
308 let mut layer = Layer::new("test");
309 layer.store_put(Metadata::new(
310 operation.to_string(),
311 "Some Sdk Id".to_string(),
312 ));
313 layer.store_put(aws_smithy_schema::protocol::ServiceShapeName::new(
314 shape_name,
315 ));
316 ConfigBag::of_layers(vec![layer])
317 }
318
319 #[test]
324 fn target_prefix_defaults_to_service_shape_name_from_config_bag() {
325 let cfg = cfg_with_service_shape_name("MyService", "DoThing");
326 let request = AwsJsonRpcProtocol::aws_json_1_0()
327 .serialize_request(&EmptyStruct, &TEST_SCHEMA, "/", &cfg)
328 .unwrap();
329 assert_eq!(
330 request.headers().get("X-Amz-Target").unwrap(),
331 "MyService.DoThing"
332 );
333 }
334
335 #[test]
338 fn with_target_prefix_overrides_config_bag() {
339 let cfg = cfg_with_service_shape_name("MyService", "DoThing");
340 let request = AwsJsonRpcProtocol::aws_json_1_0()
341 .with_target_prefix("DynamoDB_20120810")
342 .serialize_request(&EmptyStruct, &TEST_SCHEMA, "/", &cfg)
343 .unwrap();
344 assert_eq!(
345 request.headers().get("X-Amz-Target").unwrap(),
346 "DynamoDB_20120810.DoThing"
347 );
348 }
349
350 #[test]
353 fn x_amz_target_omitted_when_prefix_is_unknown() {
354 let cfg = cfg_with_metadata("Some Sdk Id", "DoThing");
355 let request = AwsJsonRpcProtocol::aws_json_1_0()
356 .serialize_request(&EmptyStruct, &TEST_SCHEMA, "/", &cfg)
357 .unwrap();
358 assert!(request.headers().get("X-Amz-Target").is_none());
359 }
360
361 #[test]
362 fn json_1_0_content_type() {
363 let request = AwsJsonRpcProtocol::aws_json_1_0()
364 .serialize_request(
365 &EmptyStruct,
366 &TEST_SCHEMA,
367 "https://example.com",
368 &ConfigBag::base(),
369 )
370 .unwrap();
371 assert_eq!(
372 request.headers().get("Content-Type").unwrap(),
373 "application/x-amz-json-1.0"
374 );
375 }
376
377 #[test]
378 fn json_1_1_content_type() {
379 let request = AwsJsonRpcProtocol::aws_json_1_1()
380 .serialize_request(
381 &EmptyStruct,
382 &TEST_SCHEMA,
383 "https://example.com",
384 &ConfigBag::base(),
385 )
386 .unwrap();
387 assert_eq!(
388 request.headers().get("Content-Type").unwrap(),
389 "application/x-amz-json-1.1"
390 );
391 }
392
393 #[test]
394 fn sets_x_amz_target() {
395 let cfg = cfg_with_metadata("MyService", "DoThing");
396 let request = AwsJsonRpcProtocol::aws_json_1_0()
397 .with_target_prefix("MyService")
398 .serialize_request(&EmptyStruct, &TEST_SCHEMA, "https://example.com", &cfg)
399 .unwrap();
400 assert_eq!(
401 request.headers().get("X-Amz-Target").unwrap(),
402 "MyService.DoThing"
403 );
404 }
405
406 #[test]
410 fn malformed_response_bodies_are_rejected() {
411 use aws_smithy_runtime_api::http::Response;
412 use aws_smithy_types::body::SdkBody;
413
414 for body in [r#"{ "int": 10 }abc"#, r#"{"int": 10,}"#] {
415 let response = Response::new(200u16.try_into().unwrap(), SdkBody::from(body));
416 let base_cfg = ConfigBag::base();
417 let mut deser = AwsJsonRpcProtocol::aws_json_1_0()
418 .deserialize_response(&response, &TEST_SCHEMA, &base_cfg)
419 .unwrap();
420 let result = deser.read_struct(&TEST_SCHEMA, &mut |_, _| Ok(()));
421 assert!(result.is_err(), "body {body:?} must be rejected");
422 }
423
424 let response = Response::new(
425 200u16.try_into().unwrap(),
426 SdkBody::from(r#"{ "int": 10 }"#),
427 );
428 let base_cfg = ConfigBag::base();
429 let mut deser = AwsJsonRpcProtocol::aws_json_1_0()
430 .deserialize_response(&response, &TEST_SCHEMA, &base_cfg)
431 .unwrap();
432 deser
433 .read_struct(&TEST_SCHEMA, &mut |_, _| Ok(()))
434 .expect("well-formed body is accepted");
435 }
436
437 #[test]
440 fn quoted_number_for_double_member_is_rejected() {
441 use aws_smithy_runtime_api::http::Response;
442 use aws_smithy_types::body::SdkBody;
443
444 static DOUBLE: aws_smithy_schema::Schema = aws_smithy_schema::Schema::new_member(
445 shape_id!("test", "Output", "doubleInBody"),
446 ShapeType::Double,
447 "doubleInBody",
448 0,
449 );
450 static OUTPUT: aws_smithy_schema::Schema = aws_smithy_schema::Schema::new_struct(
451 shape_id!("test", "Output"),
452 ShapeType::Structure,
453 &[&DOUBLE],
454 );
455 let read = |body: &str| {
456 let response = Response::new(200u16.try_into().unwrap(), SdkBody::from(body));
457 let base_cfg = ConfigBag::base();
458 let mut deser = AwsJsonRpcProtocol::aws_json_1_0()
459 .deserialize_response(&response, &OUTPUT, &base_cfg)
460 .unwrap();
461 let mut value = None;
462 deser
463 .read_struct(&OUTPUT, &mut |member, d| {
464 value = Some(d.read_double(member)?);
465 Ok(())
466 })
467 .map(|()| value)
468 };
469 assert_eq!(read(r#"{ "doubleInBody" : 123 }"#).unwrap(), Some(123.0));
470 assert!(read(r#"{ "doubleInBody" : "123" }"#).is_err());
471 }
472
473 #[test]
474 fn json_1_0_protocol_id() {
475 assert_eq!(
476 AwsJsonRpcProtocol::aws_json_1_0().protocol_id().as_str(),
477 "aws.protocols#awsJson1_0"
478 );
479 }
480
481 #[test]
491 fn serialize_request_ignores_a_route_computed_for_another_protocol() {
492 let cfg = cfg_with_metadata("MyService", "DoThing");
493 for foreign_route in ["/service/MyService/operation/DoThing", "/stats"] {
494 let request = AwsJsonRpcProtocol::aws_json_1_0()
495 .serialize_request(&EmptyStruct, &TEST_SCHEMA, foreign_route, &cfg)
496 .unwrap();
497 assert_eq!(
498 "/",
499 request.uri(),
500 "awsJson must POST to / regardless of the path it is handed"
501 );
502 assert_eq!("POST", request.method());
503 }
504 }
505
506 #[test]
508 fn serialize_request_defaults_to_slash() {
509 let cfg = cfg_with_metadata("MyService", "DoThing");
510 let request = AwsJsonRpcProtocol::aws_json_1_0()
511 .serialize_request(&EmptyStruct, &TEST_SCHEMA, "", &cfg)
512 .unwrap();
513 assert_eq!("/", request.uri());
514 }
515
516 #[test]
517 fn json_1_1_protocol_id() {
518 assert_eq!(
519 AwsJsonRpcProtocol::aws_json_1_1().protocol_id().as_str(),
520 "aws.protocols#awsJson1_1"
521 );
522 }
523
524 use aws_smithy_runtime_api::http::{Response, StatusCode};
527 use aws_smithy_types::body::SdkBody;
528
529 fn http_response(headers: &[(&str, &str)], body: &str) -> Response {
530 let mut response = Response::new(StatusCode::try_from(400).unwrap(), SdkBody::from(body));
531 for (name, value) in headers {
532 response
533 .headers_mut()
534 .insert(name.to_string(), value.to_string());
535 }
536 response
537 }
538
539 #[test]
540 fn parse_error_metadata_extracts_code_and_message_from_body() {
541 let proto = AwsJsonRpcProtocol::aws_json_1_0();
542 let response = http_response(&[], r#"{"__type":"InvalidGreeting","message":"hi"}"#);
543 let cfg = ConfigBag::base();
544 let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
545 assert_eq!(meta.code(), Some("InvalidGreeting"));
546 assert_eq!(meta.message(), Some("hi"));
547 }
548
549 #[test]
550 fn parse_error_metadata_header_takes_priority() {
551 let proto = AwsJsonRpcProtocol::aws_json_1_1();
552 let response = http_response(
553 &[("x-amzn-errortype", "FromHeader")],
554 r#"{"__type":"FromBody","message":"go"}"#,
555 );
556 let cfg = ConfigBag::base();
557 let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
558 assert_eq!(meta.code(), Some("FromHeader"));
559 assert_eq!(meta.message(), Some("go"));
560 }
561
562 #[test]
563 fn parse_error_metadata_sanitizes_namespaced_code() {
564 let proto = AwsJsonRpcProtocol::aws_json_1_0();
565 let response = http_response(&[], r#"{"__type":"aws.protocoltests.json#FooError"}"#);
566 let cfg = ConfigBag::base();
567 let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
568 assert_eq!(meta.code(), Some("FooError"));
569 }
570
571 #[test]
572 fn parse_error_metadata_empty_body_returns_empty_builder() {
573 let proto = AwsJsonRpcProtocol::aws_json_1_0();
574 let response = http_response(&[], "");
575 let cfg = ConfigBag::base();
576 let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
577 assert!(meta.code().is_none());
578 assert!(meta.message().is_none());
579 }
580
581 #[test]
582 fn parse_error_metadata_malformed_body_returns_error() {
583 let proto = AwsJsonRpcProtocol::aws_json_1_0();
584 let response = http_response(&[], r#"{"__type":"FooError""#); let cfg = ConfigBag::base();
586 let err = proto.parse_error_metadata(&response, &cfg).unwrap_err();
587 assert!(matches!(err, SerdeError::InvalidInput { .. }));
588 }
589
590 #[test]
591 fn with_default_namespace_propagates_to_codec_settings() {
592 let proto = AwsJsonRpcProtocol::aws_json_1_0()
598 .with_target_prefix("DynamoDB_20120810")
599 .with_default_namespace("com.amazonaws.dynamodb");
600 assert_eq!(
601 proto.inner.codec().settings().default_namespace(),
602 Some("com.amazonaws.dynamodb"),
603 );
604 }
605
606 #[test]
607 fn with_default_namespace_preserves_other_settings() {
608 let proto = AwsJsonRpcProtocol::aws_json_1_0().with_default_namespace("com.example");
613 let settings = proto.inner.codec().settings();
614 assert_eq!(settings.default_namespace(), Some("com.example"));
615 assert_eq!(
616 settings.default_timestamp_format(),
617 aws_smithy_types::date_time::Format::EpochSeconds,
618 );
619 }
620
621 #[test]
632 fn the_namespace_branch_still_ignores_http_response_bindings() {
633 use aws_smithy_runtime_api::http::{Response, StatusCode};
634 use aws_smithy_schema::protocol::ServiceShapeNamespace;
635 use aws_smithy_types::body::SdkBody;
636
637 static NS_NAME: Schema<'static> =
638 Schema::new_member(shape_id!("test", "Out"), ShapeType::String, "name", 0)
639 .with_http_header("x-name");
640 static NS_STATUS: Schema<'static> =
641 Schema::new_member(shape_id!("test", "Out"), ShapeType::Integer, "status", 1)
642 .with_http_response_code();
643 static NS_OUT: Schema<'static> = Schema::new_struct(
644 shape_id!("test", "Out"),
645 ShapeType::Structure,
646 &[&NS_NAME, &NS_STATUS],
647 );
648
649 let proto = AwsJsonRpcProtocol::aws_json_1_0();
650 let mut layer = Layer::new("test");
651 layer.store_put(ServiceShapeNamespace::new("com.amazonaws.dynamodb"));
652 let cfg = ConfigBag::of_layers(vec![layer]);
653 assert!(
654 crate::protocol::codec_with_bag_namespace(proto.inner.codec(), &cfg).is_some(),
655 "precondition: the namespace branch is the one being exercised"
656 );
657
658 let mut response = Response::new(
659 StatusCode::try_from(418).unwrap(),
660 SdkBody::from(r#"{"name":"from-body"}"#),
661 );
662 response
663 .headers_mut()
664 .insert("x-name", "from-header".to_string());
665
666 let mut name = None;
667 let mut status = None;
668 proto
669 .deserialize_response(&response, &NS_OUT, &cfg)
670 .unwrap()
671 .read_struct(&NS_OUT, &mut |member, d| {
672 match member.member_name() {
673 Some("name") => name = Some(d.read_string(member)?),
674 Some("status") => status = Some(d.read_integer(member)?),
675 _ => {
676 d.skip_value()?;
677 }
678 }
679 Ok(())
680 })
681 .unwrap();
682
683 assert_eq!(
684 name.as_deref(),
685 Some("from-body"),
686 "a bound member must come from the JSON body, never from the header"
687 );
688 assert_eq!(
689 status, None,
690 "an `@httpResponseCode` member has no body representation here, so it stays absent \
691 rather than picking up the response's status"
692 );
693 }
694}