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::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)]
112mod 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}