1 + | /*
|
2 + | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 + | * SPDX-License-Identifier: Apache-2.0
|
4 + | */
|
5 + |
|
6 + | //! AWS JSON RPC protocol implementation (`awsJson1_0` and `awsJson1_1`).
|
7 + | //!
|
8 + | //! # Protocol behaviors
|
9 + | //!
|
10 + | //! - HTTP method: always POST, path: always `/`
|
11 + | //! - `X-Amz-Target`: `{ServiceName}.{OperationName}` (required)
|
12 + | //! - Does **not** use `@jsonName` trait
|
13 + | //! - Default timestamp format: `epoch-seconds`
|
14 + | //! - Ignores HTTP binding traits
|
15 + | //!
|
16 + | //! # Differences between 1.0 and 1.1
|
17 + | //!
|
18 + | //! - Content-Type: `application/x-amz-json-1.0` vs `application/x-amz-json-1.1`
|
19 + | //! - Error `__type` serialization differs on the server side, but clients MUST
|
20 + | //! accept either format for both versions.
|
21 + |
|
22 + | use crate::codec::{JsonCodec, JsonCodecSettings};
|
23 + | use aws_smithy_runtime_api::client::orchestrator::Metadata;
|
24 + | use aws_smithy_schema::http_protocol::HttpRpcProtocol;
|
25 + | use aws_smithy_schema::{shape_id, Schema, ShapeId};
|
26 + | use aws_smithy_types::config_bag::ConfigBag;
|
27 + |
|
28 + | /// AWS JSON RPC protocol (`awsJson1_0` / `awsJson1_1`).
|
29 + | #[derive(Debug)]
|
30 + | pub struct AwsJsonRpcProtocol {
|
31 + | inner: HttpRpcProtocol<JsonCodec>,
|
32 + | target_prefix: String,
|
33 + | }
|
34 + |
|
35 + | impl AwsJsonRpcProtocol {
|
36 + | /// Creates an AWS JSON 1.0 protocol instance.
|
37 + | ///
|
38 + | /// `target_prefix` is the Smithy service shape name used in the `X-Amz-Target` header
|
39 + | /// (e.g., `"TrentService"` for KMS, `"DynamoDB_20120810"` for DynamoDB).
|
40 + | pub fn aws_json_1_0(target_prefix: impl Into<String>) -> Self {
|
41 + | Self::new(
|
42 + | shape_id!("aws.protocols", "awsJson1_0"),
|
43 + | "application/x-amz-json-1.0",
|
44 + | target_prefix.into(),
|
45 + | )
|
46 + | }
|
47 + |
|
48 + | /// Creates an AWS JSON 1.1 protocol instance.
|
49 + | ///
|
50 + | /// `target_prefix` is the Smithy service shape name used in the `X-Amz-Target` header.
|
51 + | pub fn aws_json_1_1(target_prefix: impl Into<String>) -> Self {
|
52 + | Self::new(
|
53 + | shape_id!("aws.protocols", "awsJson1_1"),
|
54 + | "application/x-amz-json-1.1",
|
55 + | target_prefix.into(),
|
56 + | )
|
57 + | }
|
58 + |
|
59 + | fn new(protocol_id: ShapeId, content_type: &'static str, target_prefix: String) -> Self {
|
60 + | let codec = JsonCodec::new(
|
61 + | JsonCodecSettings::builder()
|
62 + | .use_json_name(false)
|
63 + | .default_timestamp_format(aws_smithy_types::date_time::Format::EpochSeconds)
|
64 + | .build(),
|
65 + | );
|
66 + | Self {
|
67 + | inner: HttpRpcProtocol::new(protocol_id, codec, content_type),
|
68 + | target_prefix,
|
69 + | }
|
70 + | }
|
71 + | }
|
72 + |
|
73 + | impl aws_smithy_schema::protocol::ClientProtocol for AwsJsonRpcProtocol {
|
74 + | fn protocol_id(&self) -> &ShapeId {
|
75 + | self.inner.protocol_id()
|
76 + | }
|
77 + |
|
78 + | fn serialize_request(
|
79 + | &self,
|
80 + | input: &dyn aws_smithy_schema::serde::SerializableStruct,
|
81 + | input_schema: &Schema,
|
82 + | endpoint: &str,
|
83 + | cfg: &ConfigBag,
|
84 + | ) -> Result<aws_smithy_runtime_api::http::Request, aws_smithy_schema::serde::SerdeError> {
|
85 + | let mut request = self
|
86 + | .inner
|
87 + | .serialize_request(input, input_schema, endpoint, cfg)?;
|
88 + | if let Some(metadata) = cfg.load::<Metadata>() {
|
89 + | request.headers_mut().insert(
|
90 + | "X-Amz-Target",
|
91 + | format!("{}.{}", self.target_prefix, metadata.name()),
|
92 + | );
|
93 + | }
|
94 + | Ok(request)
|
95 + | }
|
96 + |
|
97 + | fn deserialize_response<'a>(
|
98 + | &self,
|
99 + | response: &'a aws_smithy_runtime_api::http::Response,
|
100 + | output_schema: &Schema,
|
101 + | cfg: &ConfigBag,
|
102 + | ) -> Result<
|
103 + | Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
|
104 + | aws_smithy_schema::serde::SerdeError,
|
105 + | > {
|
106 + | self.inner
|
107 + | .deserialize_response(response, output_schema, cfg)
|
108 + | }
|
109 + | }
|
110 + |
|
111 + | #[cfg(test)]
|
112 + | mod tests {
|
113 + | use super::*;
|
114 + | use aws_smithy_schema::protocol::ClientProtocol;
|
115 + | use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeSerializer};
|
116 + | use aws_smithy_schema::ShapeType;
|
117 + | use aws_smithy_types::config_bag::Layer;
|
118 + |
|
119 + | struct EmptyStruct;
|
120 + | impl SerializableStruct for EmptyStruct {
|
121 + | fn serialize_members(&self, _: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
|
122 + | Ok(())
|
123 + | }
|
124 + | }
|
125 + |
|
126 + | static TEST_SCHEMA: aws_smithy_schema::Schema =
|
127 + | aws_smithy_schema::Schema::new(shape_id!("test", "Input"), ShapeType::Structure);
|
128 + |
|
129 + | fn cfg_with_metadata(service: &str, operation: &str) -> ConfigBag {
|
130 + | let mut layer = Layer::new("test");
|
131 + | layer.store_put(Metadata::new(operation.to_string(), service.to_string()));
|
132 + | ConfigBag::of_layers(vec![layer])
|
133 + | }
|
134 + |
|
135 + | #[test]
|
136 + | fn json_1_0_content_type() {
|
137 + | let request = AwsJsonRpcProtocol::aws_json_1_0("TestService")
|
138 + | .serialize_request(
|
139 + | &EmptyStruct,
|
140 + | &TEST_SCHEMA,
|
141 + | "https://example.com",
|
142 + | &ConfigBag::base(),
|
143 + | )
|
144 + | .unwrap();
|
145 + | assert_eq!(
|
146 + | request.headers().get("Content-Type").unwrap(),
|
147 + | "application/x-amz-json-1.0"
|
148 + | );
|
149 + | }
|
150 + |
|
151 + | #[test]
|
152 + | fn json_1_1_content_type() {
|
153 + | let request = AwsJsonRpcProtocol::aws_json_1_1("TestService")
|
154 + | .serialize_request(
|
155 + | &EmptyStruct,
|
156 + | &TEST_SCHEMA,
|
157 + | "https://example.com",
|
158 + | &ConfigBag::base(),
|
159 + | )
|
160 + | .unwrap();
|
161 + | assert_eq!(
|
162 + | request.headers().get("Content-Type").unwrap(),
|
163 + | "application/x-amz-json-1.1"
|
164 + | );
|
165 + | }
|
166 + |
|
167 + | #[test]
|
168 + | fn sets_x_amz_target() {
|
169 + | let cfg = cfg_with_metadata("MyService", "DoThing");
|
170 + | let request = AwsJsonRpcProtocol::aws_json_1_0("MyService")
|
171 + | .serialize_request(&EmptyStruct, &TEST_SCHEMA, "https://example.com", &cfg)
|
172 + | .unwrap();
|
173 + | assert_eq!(
|
174 + | request.headers().get("X-Amz-Target").unwrap(),
|
175 + | "MyService.DoThing"
|
176 + | );
|
177 + | }
|
178 + |
|
179 + | #[test]
|
180 + | fn json_1_0_protocol_id() {
|
181 + | assert_eq!(
|
182 + | AwsJsonRpcProtocol::aws_json_1_0("Svc")
|
183 + | .protocol_id()
|
184 + | .as_str(),
|
185 + | "aws.protocols#awsJson1_0"
|
186 + | );
|
187 + | }
|
188 + |
|
189 + | #[test]
|
190 + | fn json_1_1_protocol_id() {
|
191 + | assert_eq!(
|
192 + | AwsJsonRpcProtocol::aws_json_1_1("Svc")
|
193 + | .protocol_id()
|
194 + | .as_str(),
|
195 + | "aws.protocols#awsJson1_1"
|
196 + | );
|
197 + | }
|
198 + | }
|