1use aws_smithy_runtime_api::client::orchestrator::Metadata;
7use aws_smithy_runtime_api::http::{Request, Response};
8use aws_smithy_schema::protocol::{apply_http_endpoint, ClientProtocolInner, ServiceVersion};
9use aws_smithy_schema::serde::{
10 SerdeError, SerializableStruct, ShapeDeserializer, ShapeSerializer,
11};
12use aws_smithy_schema::{shape_id, Schema, ShapeId};
13use aws_smithy_types::body::SdkBody;
14use aws_smithy_types::config_bag::ConfigBag;
15use aws_smithy_xml::codec::find_depth2_element_slice_by;
16
17use crate::codec::serializer::QueryShapeSerializer;
18
19#[derive(Debug)]
20pub struct AwsQueryProtocol {
21 protocol_id: ShapeId<'static>,
22 service_version: Option<String>,
25}
26
27impl AwsQueryProtocol {
28 pub fn new() -> Self {
33 Self {
34 protocol_id: shape_id!("aws.protocols", "awsQuery"),
35 service_version: None,
36 }
37 }
38
39 pub fn with_service_version(mut self, version: impl Into<String>) -> Self {
46 self.service_version = Some(version.into());
47 self
48 }
49}
50
51impl Default for AwsQueryProtocol {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl ClientProtocolInner for AwsQueryProtocol {
58 type Request = Request;
59 type Response = Response;
60
61 fn protocol_id(&self) -> &ShapeId<'static> {
62 &self.protocol_id
63 }
64
65 fn serialize_request(
74 &self,
75 input: &dyn SerializableStruct,
76 input_schema: &Schema<'_>,
77 _endpoint: &str,
78 cfg: &ConfigBag,
79 ) -> Result<Request, SerdeError> {
80 let op_name = cfg
81 .load::<Metadata>()
82 .map(|m| m.name().to_string())
83 .ok_or_else(|| {
84 SerdeError::custom(
85 "operation Metadata is required to serialize an awsQuery request (Action=)",
86 )
87 })?;
88
89 let service_version = self
90 .service_version
91 .as_deref()
92 .or_else(|| cfg.load::<ServiceVersion>().map(ServiceVersion::as_str))
93 .ok_or_else(|| {
94 SerdeError::custom(
95 "a service version is required to serialize an awsQuery request (Version=); \
96 it is normally read from the ServiceVersion config-bag entry stored by \
97 generated clients, or can be set with AwsQueryProtocol::with_service_version",
98 )
99 })?;
100
101 let mut serializer = QueryShapeSerializer::new(&op_name, service_version);
102 serializer.write_struct(input_schema, input)?;
103 let body = aws_smithy_schema::codec::FinishSerializer::finish(serializer);
104
105 let uri = "/";
106 let mut request = Request::new(SdkBody::from(body));
107 request
108 .set_method("POST")
109 .map_err(|e| SerdeError::custom(format!("{e}")))?;
110 request
111 .set_uri(uri)
112 .map_err(|e| SerdeError::custom(format!("{e}")))?;
113 request
114 .headers_mut()
115 .insert("Content-Type", "application/x-www-form-urlencoded");
116 if let Some(len) = request.body().content_length() {
117 request
118 .headers_mut()
119 .insert("Content-Length", len.to_string());
120 }
121 Ok(request)
122 }
123
124 fn deserialize_response<'a>(
125 &self,
126 response: &'a Response,
127 _output_schema: &Schema<'_>,
128 _cfg: &'a ConfigBag,
129 ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
130 use aws_smithy_schema::codec::Codec;
131 use aws_smithy_xml::codec::{XmlCodec, XmlCodecSettings};
132
133 let body = response
134 .body()
135 .bytes()
136 .ok_or_else(|| SerdeError::custom("response body not available"))?;
137 let body_str =
138 std::str::from_utf8(body).map_err(|e| SerdeError::invalid_input(e.to_string()))?;
139
140 let inner = strip_aws_query_envelope(body_str);
146
147 let codec = XmlCodec::new(
153 XmlCodecSettings::builder()
154 .default_timestamp_format(aws_smithy_types::date_time::Format::DateTime)
155 .build(),
156 );
157 Ok(Box::new(codec.create_deserializer(inner.as_bytes())))
158 }
159
160 fn update_endpoint(
161 &self,
162 request: &mut Request,
163 endpoint: &aws_smithy_types::endpoint::Endpoint,
164 cfg: &ConfigBag,
165 ) -> Result<(), SerdeError> {
166 apply_http_endpoint(request, endpoint, cfg)
167 }
168}
169
170fn strip_aws_query_envelope(xml: &str) -> &str {
194 match find_depth2_element_slice_by(xml.as_bytes(), |name| {
195 name.ends_with("Result") || name == "Error"
196 }) {
197 Some(slice) => std::str::from_utf8(slice).unwrap_or(xml),
200 None => xml,
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use aws_smithy_schema::protocol::ClientProtocolInner;
208 use aws_smithy_schema::serde::ShapeSerializer;
209 use aws_smithy_schema::ShapeType;
210 use aws_smithy_types::config_bag::Layer;
211
212 struct EmptyInput;
213 impl SerializableStruct for EmptyInput {
214 fn schema(&self) -> &Schema<'_> {
215 &SCHEMA
216 }
217
218 fn serialize_members(&self, _: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
219 Ok(())
220 }
221 }
222
223 static SCHEMA: Schema<'static> = Schema::new(shape_id!("test", "Input"), ShapeType::Structure);
224
225 fn cfg_with_metadata() -> ConfigBag {
226 let mut layer = Layer::new("test");
227 layer.store_put(Metadata::new("GetUser", "MyService"));
228 ConfigBag::of_layers(vec![layer])
229 }
230
231 fn cfg_with_service_version(version: &'static str) -> ConfigBag {
234 let mut layer = Layer::new("test");
235 layer.store_put(Metadata::new("GetUser", "MyService"));
236 layer.store_put(aws_smithy_schema::protocol::ServiceVersion::new(version));
237 ConfigBag::of_layers(vec![layer])
238 }
239
240 #[test]
244 fn service_version_defaults_from_config_bag() {
245 let cfg = cfg_with_service_version("2012-11-05");
246 let request = AwsQueryProtocol::new()
247 .serialize_request(&EmptyInput, &SCHEMA, "/", &cfg)
248 .unwrap();
249 let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
250 assert!(
251 body.contains("Version=2012-11-05"),
252 "expected Version= from the config bag, got {body}"
253 );
254 }
255
256 #[test]
258 fn with_service_version_overrides_config_bag() {
259 let cfg = cfg_with_service_version("2012-11-05");
260 let request = AwsQueryProtocol::new()
261 .with_service_version("1999-01-01")
262 .serialize_request(&EmptyInput, &SCHEMA, "/", &cfg)
263 .unwrap();
264 let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
265 assert!(
266 body.contains("Version=1999-01-01"),
267 "explicit version must win, got {body}"
268 );
269 }
270
271 #[test]
275 fn missing_service_version_is_an_error() {
276 let cfg = cfg_with_metadata();
277 let err = AwsQueryProtocol::new()
278 .serialize_request(&EmptyInput, &SCHEMA, "/", &cfg)
279 .expect_err("no version is available");
280 assert!(
281 err.to_string().contains("version"),
282 "error should name the missing version, got: {err}"
283 );
284 }
285
286 #[test]
287 fn request_has_correct_content_type() {
288 let cfg = cfg_with_metadata();
289 let request = AwsQueryProtocol::new()
290 .with_service_version("2012-11-05")
291 .serialize_request(&EmptyInput, &SCHEMA, "https://example.com", &cfg)
292 .unwrap();
293 assert_eq!(
294 request.headers().get("Content-Type").unwrap(),
295 "application/x-www-form-urlencoded"
296 );
297 }
298
299 #[test]
300 fn request_has_action_and_version() {
301 let cfg = cfg_with_metadata();
302 let request = AwsQueryProtocol::new()
303 .with_service_version("2012-11-05")
304 .serialize_request(&EmptyInput, &SCHEMA, "https://example.com", &cfg)
305 .unwrap();
306 let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
307 assert!(body.contains("Action=GetUser"));
308 assert!(body.contains("Version=2012-11-05"));
309 }
310
311 #[test]
325 fn request_ignores_a_route_computed_for_another_protocol() {
326 let cfg = cfg_with_metadata();
327 for foreign_route in ["/service/MyService/operation/GetUser", "/stats"] {
328 let request = AwsQueryProtocol::new()
329 .with_service_version("1.0")
330 .serialize_request(&EmptyInput, &SCHEMA, foreign_route, &cfg)
331 .unwrap();
332 assert_eq!(
333 request.uri(),
334 "/",
335 "awsQuery must POST to / regardless of the path it is handed"
336 );
337 assert_eq!(request.method(), "POST");
338 }
339 }
340
341 #[test]
342 fn request_defaults_to_slash() {
343 let cfg = cfg_with_metadata();
344 let request = AwsQueryProtocol::new()
345 .with_service_version("1.0")
346 .serialize_request(&EmptyInput, &SCHEMA, "", &cfg)
347 .unwrap();
348 assert_eq!(request.uri(), "/");
349 }
350
351 #[test]
352 fn deserialize_response_strips_envelope() {
353 let xml = "<GetUserResponse><GetUserResult><Name>Alice</Name><Age>30</Age></GetUserResult></GetUserResponse>";
354 let response = Response::new(200u16.try_into().unwrap(), SdkBody::from(xml));
355
356 static NAME: Schema<'static> =
357 Schema::new_member(shape_id!("t", "S"), ShapeType::String, "Name", 0);
358 static AGE: Schema<'static> =
359 Schema::new_member(shape_id!("t", "S"), ShapeType::Integer, "Age", 1);
360 static OUT_SCHEMA: Schema<'static> =
361 Schema::new_struct(shape_id!("t", "S"), ShapeType::Structure, &[&NAME, &AGE]);
362
363 let base_cfg = ConfigBag::base();
364 let mut deser = AwsQueryProtocol::new()
365 .with_service_version("1.0")
366 .deserialize_response(&response, &OUT_SCHEMA, &base_cfg)
367 .unwrap();
368 let mut name = String::new();
369 let mut age = 0i32;
370 deser
371 .read_struct(&OUT_SCHEMA, &mut |member, d| {
372 match member.member_name() {
373 Some("Name") => name = d.read_string(member)?,
374 Some("Age") => age = d.read_integer(member)?,
375 _ => {}
376 }
377 Ok(())
378 })
379 .unwrap();
380 assert_eq!(name, "Alice");
381 assert_eq!(age, 30);
382 }
383
384 #[test]
393 fn deserialize_response_ignores_http_response_bindings() {
394 let xml = "<GetUserResponse><GetUserResult><Name>from-body</Name></GetUserResult></GetUserResponse>";
395 let mut response = Response::new(418u16.try_into().unwrap(), SdkBody::from(xml));
396 response
397 .headers_mut()
398 .insert("x-name", "from-header".to_string());
399
400 static BOUND_NAME: Schema<'static> =
401 Schema::new_member(shape_id!("t", "S"), ShapeType::String, "Name", 0)
402 .with_http_header("x-name");
403 static BOUND_STATUS: Schema<'static> =
404 Schema::new_member(shape_id!("t", "S"), ShapeType::Integer, "Status", 1)
405 .with_http_response_code();
406 static BOUND_SCHEMA: Schema<'static> = Schema::new_struct(
407 shape_id!("t", "S"),
408 ShapeType::Structure,
409 &[&BOUND_NAME, &BOUND_STATUS],
410 );
411
412 let mut name = None;
413 let mut status = None;
414 AwsQueryProtocol::new()
415 .with_service_version("1.0")
416 .deserialize_response(&response, &BOUND_SCHEMA, &ConfigBag::base())
417 .unwrap()
418 .read_struct(&BOUND_SCHEMA, &mut |member, d| {
419 match member.member_name() {
420 Some("Name") => name = Some(d.read_string(member)?),
421 Some("Status") => status = Some(d.read_integer(member)?),
422 _ => {}
423 }
424 Ok(())
425 })
426 .unwrap();
427 assert_eq!(
428 name.as_deref(),
429 Some("from-body"),
430 "a bound member must come from the XML body, never from the header"
431 );
432 assert_eq!(
433 status, None,
434 "an `@httpResponseCode` member has no body representation here, so it stays absent"
435 );
436 }
437
438 #[test]
439 fn strip_envelope_returns_self_closing_result_element() {
440 let xml = "<GetUserResponse><GetUserResult/><ResponseMetadata><RequestId>r</RequestId></ResponseMetadata></GetUserResponse>";
443 assert_eq!(strip_aws_query_envelope(xml), "<GetUserResult/>");
444 }
445
446 #[test]
447 fn strip_envelope_returns_error_element() {
448 let xml = "<ErrorResponse><Error><Code>Boom</Code></Error></ErrorResponse>";
449 assert_eq!(
450 strip_aws_query_envelope(xml),
451 "<Error><Code>Boom</Code></Error>"
452 );
453 }
454
455 #[test]
456 fn protocol_id() {
457 assert_eq!(
458 AwsQueryProtocol::new()
459 .with_service_version("1.0")
460 .protocol_id()
461 .as_str(),
462 "aws.protocols#awsQuery"
463 );
464 }
465}