use std::sync::Arc;
use crate::codec::{XmlCodec, XmlCodecSettings, XmlDeserializer};
use crate::decode::{try_data, Document};
use aws_smithy_schema::http_protocol::{http_error_deserializer, HttpBindingProtocol};
use aws_smithy_schema::serde::SerdeError;
use aws_smithy_schema::Schema;
use aws_smithy_schema::{shape_id, ShapeId};
use aws_smithy_types::config_bag::ConfigBag;
use aws_smithy_types::date_time::Format as TimestampFormat;
use aws_smithy_types::error::metadata::{Builder as ErrorMetadataBuilder, ErrorMetadata};
static PROTOCOL_ID: ShapeId<'static> = shape_id!("aws.protocols", "restXml");
#[derive(Debug)]
pub struct AwsRestXmlProtocol {
inner: HttpBindingProtocol<XmlCodec>,
settings: Arc<XmlCodecSettings>,
no_error_wrapping: bool,
service_xml_namespace: Option<(String, Option<String>)>,
}
impl AwsRestXmlProtocol {
pub fn new() -> Self {
let settings = XmlCodecSettings::builder()
.default_timestamp_format(TimestampFormat::DateTime)
.build();
let settings = Arc::new(settings);
let codec = XmlCodec::from_shared_settings(settings.clone());
Self {
inner: HttpBindingProtocol::new(PROTOCOL_ID.clone(), codec, "application/xml"),
settings,
no_error_wrapping: false,
service_xml_namespace: None,
}
}
pub fn with_no_error_wrapping(mut self, v: bool) -> Self {
self.no_error_wrapping = v;
self
}
pub fn no_error_wrapping(&self) -> bool {
self.no_error_wrapping
}
pub fn with_service_xml_namespace(
mut self,
uri: impl Into<String>,
prefix: Option<String>,
) -> Self {
self.service_xml_namespace = Some((uri.into(), prefix));
self
}
fn resolved_service_xml_namespace(&self, cfg: &ConfigBag) -> Option<(String, Option<String>)> {
if let Some((uri, prefix)) = &self.service_xml_namespace {
return Some((uri.clone(), prefix.clone()));
}
cfg.load::<aws_smithy_schema::protocol::ServiceXmlNamespace>()
.map(|ns| (ns.uri().to_owned(), ns.prefix().map(str::to_owned)))
}
}
pub fn find_error_element_slice(body: &[u8]) -> &[u8] {
crate::codec::find_depth2_element_slice_by(body, |name| name == "Error").unwrap_or(body)
}
impl Default for AwsRestXmlProtocol {
fn default() -> Self {
Self::new()
}
}
impl aws_smithy_schema::protocol::ClientProtocolInner for AwsRestXmlProtocol {
type Request = aws_smithy_runtime_api::http::Request;
type Response = aws_smithy_runtime_api::http::Response;
fn protocol_id(&self) -> &ShapeId<'static> {
self.inner.protocol_id()
}
fn serialize_request(
&self,
input: &dyn aws_smithy_schema::serde::SerializableStruct,
input_schema: &Schema<'_>,
endpoint: &str,
cfg: &ConfigBag,
) -> Result<aws_smithy_runtime_api::http::Request, aws_smithy_schema::serde::SerdeError> {
let payload_xml_name = input_schema.members().iter().find_map(|m| {
if m.http_payload().is_some()
&& matches!(
m.shape_type(),
aws_smithy_schema::ShapeType::Structure | aws_smithy_schema::ShapeType::Union
)
{
m.xml_name().map(|n| n.value().to_owned())
} else {
None
}
});
let mut body = aws_smithy_schema::codec::Codec::create_serializer(self.inner.codec());
if let Some(name) = payload_xml_name {
body.set_next_root_xml_name(name);
}
if let Some((uri, prefix)) = self.resolved_service_xml_namespace(cfg) {
body.set_next_root_xml_namespace(uri, prefix);
}
self.inner
.serialize_request_with_body(body, input, input_schema, endpoint, cfg)
}
fn deserialize_response<'a>(
&self,
response: &'a aws_smithy_runtime_api::http::Response,
output_schema: &Schema<'_>,
cfg: &'a ConfigBag,
) -> Result<
Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
aws_smithy_schema::serde::SerdeError,
> {
self.inner
.deserialize_response(response, output_schema, cfg)
}
fn parse_error_metadata(
&self,
response: &aws_smithy_runtime_api::http::Response,
_cfg: &ConfigBag,
) -> Result<ErrorMetadataBuilder, SerdeError> {
parse_error_envelope_metadata(
response.body().bytes().unwrap_or(&[]),
self.no_error_wrapping,
)
}
fn deserialize_error_response<'a>(
&self,
response: &'a aws_smithy_runtime_api::http::Response,
cfg: &'a ConfigBag,
) -> Result<
Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
aws_smithy_schema::serde::SerdeError,
> {
let body = response.body().bytes().unwrap_or(&[]);
let inner = if self.no_error_wrapping {
body
} else {
find_error_element_slice(body)
};
Ok(http_error_deserializer(
XmlDeserializer::new(inner, self.settings.clone()),
response,
cfg,
))
}
fn payload_codec(&self) -> Option<&dyn aws_smithy_schema::codec::DynCodec> {
self.inner.payload_codec()
}
fn event_stream_media_type(&self) -> Option<&str> {
Some("application/xml")
}
fn parse_event_stream_error_metadata(
&self,
payload: &[u8],
) -> Result<ErrorMetadataBuilder, SerdeError> {
parse_error_envelope_metadata(payload, self.no_error_wrapping)
}
fn update_endpoint(
&self,
request: &mut aws_smithy_runtime_api::http::Request,
endpoint: &aws_smithy_types::endpoint::Endpoint,
cfg: &ConfigBag,
) -> Result<(), aws_smithy_schema::serde::SerdeError> {
self.inner.update_endpoint(request, endpoint, cfg)
}
}
fn parse_error_envelope_metadata(
body: &[u8],
no_error_wrapping: bool,
) -> Result<ErrorMetadataBuilder, SerdeError> {
if body.is_empty() {
return Ok(ErrorMetadata::builder());
}
let body_str =
std::str::from_utf8(body).map_err(|e| SerdeError::custom(format!("invalid UTF-8: {e}")))?;
let mut doc = Document::new(body_str);
let mut root = doc
.root_element()
.map_err(|e| SerdeError::custom(format!("{e}")))?;
let mut builder = ErrorMetadata::builder();
if no_error_wrapping {
while let Some(mut tag) = root.next_tag() {
match tag.start_el().local() {
"Code" => {
builder = builder
.code(try_data(&mut tag).map_err(|e| SerdeError::custom(format!("{e}")))?);
}
"Message" => {
builder = builder.message(
try_data(&mut tag).map_err(|e| SerdeError::custom(format!("{e}")))?,
);
}
_ => {}
}
}
} else {
while let Some(mut tag) = root.next_tag() {
match tag.start_el().local() {
"Error" => {
while let Some(mut error_field) = tag.next_tag() {
match error_field.start_el().local() {
"Code" => {
builder = builder.code(
try_data(&mut error_field)
.map_err(|e| SerdeError::custom(format!("{e}")))?,
);
}
"Message" => {
builder = builder.message(
try_data(&mut error_field)
.map_err(|e| SerdeError::custom(format!("{e}")))?,
);
}
_ => {}
}
}
}
"RequestId" => {
builder = builder.custom(
"request_id",
try_data(&mut tag).map_err(|e| SerdeError::custom(format!("{e}")))?,
);
}
_ => {}
}
}
}
Ok(builder)
}
#[cfg(test)]
mod tests {
use super::*;
use aws_smithy_schema::protocol::ClientProtocolInner;
use aws_smithy_schema::serde::{SerializableStruct, ShapeSerializer};
use aws_smithy_schema::{shape_id, ShapeType};
use aws_smithy_schema::traits::HttpTrait;
static NAME_MEMBER: Schema<'static> =
Schema::new_member(shape_id!("test", "Op$name"), ShapeType::String, "name", 0);
static OP_SCHEMA: Schema<'static> = Schema::new_struct(
shape_id!("test", "OpRequest"),
ShapeType::Structure,
&[&NAME_MEMBER],
)
.with_original_name("OpRequest")
.with_http(HttpTrait::new("PUT", "/op", None));
struct TestInput;
impl SerializableStruct for TestInput {
fn schema(&self) -> &Schema<'_> {
&OP_SCHEMA
}
fn serialize_members(
&self,
ser: &mut dyn ShapeSerializer,
) -> Result<(), aws_smithy_schema::serde::SerdeError> {
ser.write_string(&NAME_MEMBER, "Alice")
}
}
#[test]
fn serialize_request_produces_xml_body() {
let protocol = AwsRestXmlProtocol::new();
let cfg = ConfigBag::base();
let request = protocol
.serialize_request(&TestInput, &OP_SCHEMA, "", &cfg)
.unwrap();
assert_eq!(request.method(), "PUT");
assert_eq!(request.uri(), "/op");
assert_eq!(
request.headers().get("content-type").unwrap(),
"application/xml"
);
let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
assert_eq!(body, "<OpRequest><name>Alice</name></OpRequest>");
}
#[test]
fn service_xml_namespace_defaults_from_config_bag() {
let mut layer = aws_smithy_types::config_bag::Layer::new("test");
layer.store_put(aws_smithy_schema::protocol::ServiceXmlNamespace::new(
"http://example.com/from-bag/",
None,
));
let cfg = ConfigBag::of_layers(vec![layer]);
let request = AwsRestXmlProtocol::new()
.serialize_request(&TestInput, &OP_SCHEMA, "", &cfg)
.unwrap();
let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
assert_eq!(
body,
"<OpRequest xmlns=\"http://example.com/from-bag/\"><name>Alice</name></OpRequest>"
);
}
#[test]
fn service_xml_namespace_from_config_bag_honors_prefix() {
let mut layer = aws_smithy_types::config_bag::Layer::new("test");
layer.store_put(aws_smithy_schema::protocol::ServiceXmlNamespace::new(
"http://example.com/from-bag/",
Some("ex".into()),
));
let cfg = ConfigBag::of_layers(vec![layer]);
let request = AwsRestXmlProtocol::new()
.serialize_request(&TestInput, &OP_SCHEMA, "", &cfg)
.unwrap();
let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
assert_eq!(
body,
"<OpRequest xmlns:ex=\"http://example.com/from-bag/\"><name>Alice</name></OpRequest>"
);
}
#[test]
fn with_service_xml_namespace_overrides_config_bag() {
let mut layer = aws_smithy_types::config_bag::Layer::new("test");
layer.store_put(aws_smithy_schema::protocol::ServiceXmlNamespace::new(
"http://example.com/from-bag/",
None,
));
let cfg = ConfigBag::of_layers(vec![layer]);
let request = AwsRestXmlProtocol::new()
.with_service_xml_namespace("http://example.com/explicit/".to_owned(), None)
.serialize_request(&TestInput, &OP_SCHEMA, "", &cfg)
.unwrap();
let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
assert_eq!(
body,
"<OpRequest xmlns=\"http://example.com/explicit/\"><name>Alice</name></OpRequest>"
);
}
#[test]
fn deserialize_response_round_trips() {
let protocol = AwsRestXmlProtocol::new();
let cfg = ConfigBag::base();
let body = b"<OpResponse><name>Bob</name></OpResponse>";
let response = aws_smithy_runtime_api::http::Response::new(
aws_smithy_runtime_api::http::StatusCode::try_from(200).unwrap(),
aws_smithy_types::body::SdkBody::from(&body[..]),
);
let mut deser = protocol
.deserialize_response(&response, &OP_SCHEMA, &cfg)
.unwrap();
let mut name = String::new();
deser
.read_struct(&OP_SCHEMA, &mut |member, d| {
if member.member_name() == Some("name") {
name = d.read_string(member)?;
}
Ok(())
})
.unwrap();
assert_eq!(name, "Bob");
}
fn http_response(body: &[u8]) -> aws_smithy_runtime_api::http::Response {
aws_smithy_runtime_api::http::Response::new(
aws_smithy_runtime_api::http::StatusCode::try_from(400).unwrap(),
aws_smithy_types::body::SdkBody::from(body),
)
}
#[test]
fn protocol_id_is_rest_xml() {
assert_eq!(
AwsRestXmlProtocol::new().protocol_id().as_str(),
"aws.protocols#restXml"
);
}
#[test]
fn parse_error_metadata_wrapped() {
let protocol = AwsRestXmlProtocol::new();
let response = http_response(b"<ErrorResponse><Error><Type>Sender</Type><Code>InvalidGreeting</Code><Message>Hi</Message><Greeting>Howdy</Greeting></Error><RequestId>req-1</RequestId></ErrorResponse>");
let cfg = ConfigBag::base();
let meta = protocol
.parse_error_metadata(&response, &cfg)
.unwrap()
.build();
assert_eq!(meta.code(), Some("InvalidGreeting"));
assert_eq!(meta.message(), Some("Hi"));
assert_eq!(meta.extra("request_id"), Some("req-1"));
}
#[test]
fn parse_error_metadata_unwrapped() {
let protocol = AwsRestXmlProtocol::new().with_no_error_wrapping(true);
let response = http_response(
b"<Error><Code>NotFound</Code><Message>Gone</Message><Detail>extra</Detail></Error>",
);
let cfg = ConfigBag::base();
let meta = protocol
.parse_error_metadata(&response, &cfg)
.unwrap()
.build();
assert_eq!(meta.code(), Some("NotFound"));
assert_eq!(meta.message(), Some("Gone"));
assert_eq!(meta.extra("request_id"), None);
}
#[test]
fn parse_error_metadata_wrapped_ignores_literal_error_in_cdata() {
let protocol = AwsRestXmlProtocol::new();
let response = http_response(
b"<ErrorResponse>\
<Description><![CDATA[<Error><Code>FAKE</Code></Error>]]></Description>\
<Error><Code>RealCode</Code><Message>Real message</Message></Error>\
<RequestId>req-1</RequestId>\
</ErrorResponse>",
);
let cfg = ConfigBag::base();
let meta = protocol
.parse_error_metadata(&response, &cfg)
.unwrap()
.build();
assert_eq!(meta.code(), Some("RealCode"));
assert_eq!(meta.message(), Some("Real message"));
assert_eq!(meta.extra("request_id"), Some("req-1"));
}
#[test]
fn deserialize_error_response_positions_inside_error_wrapped() {
let protocol = AwsRestXmlProtocol::new();
let response = http_response(
b"<ErrorResponse>\
<Error xmlns=\"http://example.com/\"><name>Carol</name></Error>\
<RequestId>req-1</RequestId>\
</ErrorResponse>",
);
let cfg = ConfigBag::base();
let mut deser = protocol
.deserialize_error_response(&response, &cfg)
.unwrap();
let mut name = String::new();
deser
.read_struct(&OP_SCHEMA, &mut |member, d| {
if member.member_name() == Some("name") {
name = d.read_string(member)?;
}
Ok(())
})
.unwrap();
assert_eq!(name, "Carol");
}
#[test]
fn deserialize_error_response_positions_inside_error_unwrapped() {
let protocol = AwsRestXmlProtocol::new().with_no_error_wrapping(true);
let response = http_response(b"<Error><name>Dave</name></Error>");
let cfg = ConfigBag::base();
let mut deser = protocol
.deserialize_error_response(&response, &cfg)
.unwrap();
let mut name = String::new();
deser
.read_struct(&OP_SCHEMA, &mut |member, d| {
if member.member_name() == Some("name") {
name = d.read_string(member)?;
}
Ok(())
})
.unwrap();
assert_eq!(name, "Dave");
}
#[test]
fn find_error_element_slice_strips_wrapped_envelope() {
let body =
b"<ErrorResponse><Error><Code>X</Code></Error><RequestId>r</RequestId></ErrorResponse>";
let slice = find_error_element_slice(body);
assert_eq!(slice, b"<Error><Code>X</Code></Error>");
}
#[test]
fn find_error_element_slice_handles_xmlns_on_inner_error() {
let body = br#"<ErrorResponse><Error xmlns="http://example.com/"><Code>X</Code></Error></ErrorResponse>"#;
let slice = find_error_element_slice(body);
assert_eq!(
slice,
br#"<Error xmlns="http://example.com/"><Code>X</Code></Error>"#
);
}
#[test]
fn find_error_element_slice_returns_body_when_root_is_error() {
let body = b"<Error><Code>X</Code><Message>m</Message></Error>";
let slice = find_error_element_slice(body);
assert_eq!(slice, body);
}
#[test]
fn find_error_element_slice_falls_back_for_missing_error() {
let body = b"<SomethingElse><Code>X</Code></SomethingElse>";
let slice = find_error_element_slice(body);
assert_eq!(slice, body);
}
#[test]
fn find_error_element_slice_falls_back_for_invalid_xml() {
let body = b"not xml at all";
let slice = find_error_element_slice(body);
assert_eq!(slice, body);
}
#[test]
fn find_error_element_slice_falls_back_for_invalid_utf8() {
let body = &[0xFFu8, 0xFE, 0xFD][..];
let slice = find_error_element_slice(body);
assert_eq!(slice, body);
}
static ERR_REASON: Schema<'static> = Schema::new_member(
shape_id!("test", "Err$reason"),
ShapeType::String,
"reason",
0,
)
.with_http_header("x-reason");
static ERR_STATUS: Schema<'static> = Schema::new_member(
shape_id!("test", "Err$status"),
ShapeType::Integer,
"status",
1,
)
.with_http_response_code();
static ERR_NAME: Schema<'static> =
Schema::new_member(shape_id!("test", "Err$name"), ShapeType::String, "name", 2);
static ERR_SCHEMA: Schema<'static> = Schema::new_struct(
shape_id!("test", "Err"),
ShapeType::Structure,
&[&ERR_REASON, &ERR_STATUS, &ERR_NAME],
);
fn http_response_with_header(
name: &str,
value: &str,
body: &[u8],
) -> aws_smithy_runtime_api::http::Response {
let mut response = http_response(body);
response
.headers_mut()
.insert(name.to_string(), value.to_string());
response
}
#[test]
fn a_modeled_error_reads_its_header_and_its_body_member_from_the_envelope() {
let protocol = AwsRestXmlProtocol::new();
let response = http_response_with_header(
"x-reason",
"throttled",
b"<ErrorResponse>\
<Error><name>Carol</name></Error>\
<RequestId>req-1</RequestId>\
</ErrorResponse>",
);
let cfg = ConfigBag::base();
let mut reason = None;
let mut status = None;
let mut name = None;
protocol
.deserialize_error_response(&response, &cfg)
.unwrap()
.read_struct(&ERR_SCHEMA, &mut |member, d| {
match member.member_name() {
Some("reason") => reason = Some(d.read_string(member)?),
Some("status") => status = Some(d.read_integer(member)?),
Some("name") => name = Some(d.read_string(member)?),
_ => {}
}
Ok(())
})
.unwrap();
assert_eq!(
reason.as_deref(),
Some("throttled"),
"the header-bound member must come from the response"
);
assert_eq!(status, Some(400), "the status must come from the response");
assert_eq!(
name.as_deref(),
Some("Carol"),
"the body member must still be read from inside <Error>"
);
}
}