aws_smithy_json/protocol/
aws_json_rpc.rs

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
22use crate::codec::{JsonCodec, JsonCodecSettings};
23use aws_smithy_runtime_api::client::orchestrator::Metadata;
24use aws_smithy_schema::http_protocol::HttpRpcProtocol;
25use aws_smithy_schema::{shape_id, Schema, ShapeId};
26use aws_smithy_types::config_bag::ConfigBag;
27
28/// AWS JSON RPC protocol (`awsJson1_0` / `awsJson1_1`).
29#[derive(Debug)]
30pub struct AwsJsonRpcProtocol {
31    inner: HttpRpcProtocol<JsonCodec>,
32    target_prefix: String,
33}
34
35impl 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
73impl aws_smithy_schema::protocol::ClientProtocolInner for AwsJsonRpcProtocol {
74    type Request = aws_smithy_runtime_api::http::Request;
75    type Response = aws_smithy_runtime_api::http::Response;
76
77    fn protocol_id(&self) -> &ShapeId {
78        self.inner.protocol_id()
79    }
80
81    fn serialize_request(
82        &self,
83        input: &dyn aws_smithy_schema::serde::SerializableStruct,
84        input_schema: &Schema,
85        endpoint: &str,
86        cfg: &ConfigBag,
87    ) -> Result<aws_smithy_runtime_api::http::Request, aws_smithy_schema::serde::SerdeError> {
88        let mut request = self
89            .inner
90            .serialize_request(input, input_schema, endpoint, cfg)?;
91        if let Some(metadata) = cfg.load::<Metadata>() {
92            request.headers_mut().insert(
93                "X-Amz-Target",
94                format!("{}.{}", self.target_prefix, metadata.name()),
95            );
96        }
97        Ok(request)
98    }
99
100    fn deserialize_response<'a>(
101        &self,
102        response: &'a aws_smithy_runtime_api::http::Response,
103        output_schema: &Schema,
104        cfg: &ConfigBag,
105    ) -> Result<
106        Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
107        aws_smithy_schema::serde::SerdeError,
108    > {
109        self.inner
110            .deserialize_response(response, output_schema, cfg)
111    }
112
113    fn payload_codec(&self) -> Option<&dyn aws_smithy_schema::codec::DynCodec> {
114        self.inner.payload_codec()
115    }
116
117    fn update_endpoint(
118        &self,
119        request: &mut aws_smithy_runtime_api::http::Request,
120        endpoint: &aws_smithy_types::endpoint::Endpoint,
121        cfg: &ConfigBag,
122    ) -> Result<(), aws_smithy_schema::serde::SerdeError> {
123        self.inner.update_endpoint(request, endpoint, cfg)
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use aws_smithy_schema::protocol::ClientProtocolInner;
131    use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeSerializer};
132    use aws_smithy_schema::ShapeType;
133    use aws_smithy_types::config_bag::Layer;
134
135    struct EmptyStruct;
136    impl SerializableStruct for EmptyStruct {
137        fn serialize_members(&self, _: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
138            Ok(())
139        }
140    }
141
142    static TEST_SCHEMA: aws_smithy_schema::Schema =
143        aws_smithy_schema::Schema::new(shape_id!("test", "Input"), ShapeType::Structure);
144
145    fn cfg_with_metadata(service: &str, operation: &str) -> ConfigBag {
146        let mut layer = Layer::new("test");
147        layer.store_put(Metadata::new(operation.to_string(), service.to_string()));
148        ConfigBag::of_layers(vec![layer])
149    }
150
151    #[test]
152    fn json_1_0_content_type() {
153        let request = AwsJsonRpcProtocol::aws_json_1_0("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.0"
164        );
165    }
166
167    #[test]
168    fn json_1_1_content_type() {
169        let request = AwsJsonRpcProtocol::aws_json_1_1("TestService")
170            .serialize_request(
171                &EmptyStruct,
172                &TEST_SCHEMA,
173                "https://example.com",
174                &ConfigBag::base(),
175            )
176            .unwrap();
177        assert_eq!(
178            request.headers().get("Content-Type").unwrap(),
179            "application/x-amz-json-1.1"
180        );
181    }
182
183    #[test]
184    fn sets_x_amz_target() {
185        let cfg = cfg_with_metadata("MyService", "DoThing");
186        let request = AwsJsonRpcProtocol::aws_json_1_0("MyService")
187            .serialize_request(&EmptyStruct, &TEST_SCHEMA, "https://example.com", &cfg)
188            .unwrap();
189        assert_eq!(
190            request.headers().get("X-Amz-Target").unwrap(),
191            "MyService.DoThing"
192        );
193    }
194
195    #[test]
196    fn json_1_0_protocol_id() {
197        assert_eq!(
198            AwsJsonRpcProtocol::aws_json_1_0("Svc")
199                .protocol_id()
200                .as_str(),
201            "aws.protocols#awsJson1_0"
202        );
203    }
204
205    #[test]
206    fn json_1_1_protocol_id() {
207        assert_eq!(
208            AwsJsonRpcProtocol::aws_json_1_1("Svc")
209                .protocol_id()
210                .as_str(),
211            "aws.protocols#awsJson1_1"
212        );
213    }
214}