use super::response::{http_error_deserializer, http_output_deserializer};
use crate::codec::{Codec, FinishSerializer};
use crate::protocol::{apply_http_endpoint, ClientProtocolInner};
use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer, ShapeSerializer};
use crate::{Schema, ShapeId};
use aws_smithy_runtime_api::http::{Headers, Request, Response};
use aws_smithy_types::body::SdkBody;
use aws_smithy_types::config_bag::ConfigBag;
use std::borrow::Cow;
use std::cell::Cell;
#[derive(Debug)]
pub struct HttpBindingProtocol<C> {
protocol_id: ShapeId<'static>,
codec: C,
content_type: &'static str,
}
impl<C: Codec> HttpBindingProtocol<C> {
pub fn new(protocol_id: ShapeId<'static>, codec: C, content_type: &'static str) -> Self {
Self {
protocol_id,
codec,
content_type,
}
}
pub fn codec(&self) -> &C {
&self.codec
}
pub fn content_type(&self) -> &'static str {
self.content_type
}
pub fn with_codec(self, codec: C) -> Self {
Self {
protocol_id: self.protocol_id,
codec,
content_type: self.content_type,
}
}
pub fn serialize_request_with_body(
&self,
body: <C as Codec>::Serializer,
input: &dyn SerializableStruct,
input_schema: &Schema<'_>,
endpoint: &str,
cfg: &ConfigBag,
) -> Result<Request, SerdeError> {
let mut request = Request::new(SdkBody::empty());
let has_struct_payload = match input_schema.payload_hint() {
crate::PayloadHint::StructPayload => true,
crate::PayloadHint::NoStructPayload => false,
_ => input_schema.members().iter().any(|m| {
m.http_payload().is_some()
&& matches!(
m.shape_type(),
crate::ShapeType::Structure | crate::ShapeType::Union
)
}),
};
let skip_body_codec = !input_schema.has_body_members() && !has_struct_payload;
let (raw_payload, body_bytes, query_params, labels) = {
let mut body = body;
let mut state = BindingState::new(Some(input_schema), request.headers_mut());
if skip_body_codec || has_struct_payload {
let mut router = BindingRouter {
state: &mut state,
body: &mut body,
};
input.serialize_members(&mut router)?;
} else {
let proxy = Proxy {
state: Cell::new(Some(&mut state)),
value: input,
};
body.write_struct(input_schema, &proxy)?;
}
let raw_payload = state.raw_payload;
let body_bytes = if raw_payload.is_some() || skip_body_codec {
Vec::new()
} else {
body.finish()
};
(raw_payload, body_bytes, state.query_params, state.labels)
};
let has_blob_or_string_payload = raw_payload.is_some();
let has_body_members = has_struct_payload
|| (input_schema.has_body_members()
&& input_schema.members().iter().any(|m| {
m.http_header().is_none()
&& m.http_query().is_none()
&& m.http_label().is_none()
&& m.http_prefix_headers().is_none()
&& m.http_query_params().is_none()
&& m.http_payload().is_none()
}));
let mut body_bytes = body_bytes;
let set_content_type = if has_blob_or_string_payload {
false
} else if has_body_members {
true
} else {
body_bytes = Vec::new();
false
};
let template_opt = input_schema.http().map(|h| h.uri());
let mut uri =
String::with_capacity(endpoint.len() + template_opt.map(|t| t.len()).unwrap_or(1) + 64);
match template_opt {
Some(template) => {
append_uri_with_labels(template, &labels, &mut uri)?;
}
None => {
if endpoint.is_empty() {
uri.push('/');
} else {
append_uri_with_labels(endpoint, &labels, &mut uri)?;
}
}
}
if !query_params.is_empty() {
uri.push(if uri.contains('?') { '&' } else { '?' });
let mut first = true;
for (k, v) in &query_params {
if !first {
uri.push('&');
}
percent_encode_into(k, &mut uri);
uri.push('=');
percent_encode_into(v, &mut uri);
first = false;
}
}
*request.body_mut() = if let Some(payload) = raw_payload {
SdkBody::from(payload.into_bytes())
} else {
SdkBody::from(body_bytes)
};
if let Some(http) = input_schema.http() {
request
.set_method(http.method())
.map_err(|e| SerdeError::custom(format!("invalid HTTP method: {e}")))?;
}
request
.set_uri(uri.as_str())
.map_err(|e| SerdeError::custom(format!("invalid endpoint URI: {e}")))?;
let omit = cfg.load::<crate::header_omit_settings::SharedHeaderOmitSettings>();
let omit_content_type = omit
.map(|s| s.should_omit_default_content_type())
.unwrap_or(false);
let omit_content_length = omit
.map(|s| s.should_omit_default_content_length())
.unwrap_or(false);
if !omit_content_type && set_content_type && request.headers().get("Content-Type").is_none()
{
request
.headers_mut()
.insert("Content-Type", self.content_type);
}
if !omit_content_length {
if let Some(len) = request.body().content_length() {
if (len > 0 || set_content_type)
&& request.headers().get("Content-Length").is_none()
{
request
.headers_mut()
.insert("Content-Length", len.to_string());
}
}
}
Ok(request)
}
}
pub fn percent_encode(input: &str) -> String {
let mut out = String::with_capacity(input.len());
percent_encode_into(input, &mut out);
out
}
pub(crate) fn percent_encode_into(input: &str, out: &mut String) {
let bytes = input.as_bytes();
let mut start = 0usize;
for (i, &b) in bytes.iter().enumerate() {
let safe = matches!(
b,
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~'
);
if !safe {
if start < i {
out.push_str(&input[start..i]);
}
out.push('%');
out.push(char::from(HEX[(b >> 4) as usize]));
out.push(char::from(HEX[(b & 0x0f) as usize]));
start = i + 1;
}
}
if start < bytes.len() {
out.push_str(&input[start..]);
}
}
fn append_uri_with_labels<'sc>(
template: &str,
labels: &[(Cow<'sc, str>, String)],
out: &mut String,
) -> Result<(), SerdeError> {
let mut rem = template;
while let Some(open) = rem.find('{') {
out.push_str(&rem[..open]);
let after_open = &rem[open + 1..];
let close = match after_open.find('}') {
Some(c) => c,
None => {
out.push('{');
rem = after_open;
continue;
}
};
let label = &after_open[..close];
let (name, greedy) = match label.strip_suffix('+') {
Some(n) => (n, true),
None => (label, false),
};
let value = labels
.iter()
.find(|(n, _)| n.as_ref() == name)
.map(|(_, v)| v.as_str());
let v = match value {
Some(v) if !v.is_empty() => v,
_ => {
return Err(SerdeError::invalid_input(format!(
"{name} was missing: cannot be empty or unset"
)))
}
};
if greedy {
let mut first = true;
for seg in v.split('/') {
if !first {
out.push('/');
}
percent_encode_into(seg, out);
first = false;
}
} else {
percent_encode_into(v, out);
}
rem = &after_open[close + 1..];
}
if !rem.is_empty() {
out.push_str(rem);
}
Ok(())
}
pub(crate) const HEX: &[u8; 16] = b"0123456789ABCDEF";
struct BindingState<'a> {
headers: &'a mut Headers,
query_params: Vec<(Cow<'a, str>, String)>,
labels: Vec<(Cow<'a, str>, String)>,
input_schema: Option<&'a Schema<'a>>,
raw_payload: Option<aws_smithy_types::Blob>,
visited_bound_members: VisitedMembers,
}
#[derive(Debug)]
struct VisitedMembers {
inline: [u32; Self::INLINE_CAPACITY],
inline_len: u8,
overflow: Vec<u32>,
}
impl VisitedMembers {
const INLINE_CAPACITY: usize = 24;
const fn new() -> Self {
Self {
inline: [0; Self::INLINE_CAPACITY],
inline_len: 0,
overflow: Vec::new(),
}
}
fn insert(&mut self, idx: usize) -> bool {
let idx = idx.min(u32::MAX as usize) as u32;
let len = self.inline_len as usize;
if self.inline[..len].contains(&idx) {
return false;
}
if !self.overflow.is_empty() && self.overflow.contains(&idx) {
return false;
}
if len < Self::INLINE_CAPACITY {
self.inline[len] = idx;
self.inline_len += 1;
} else {
self.overflow.push(idx);
}
true
}
}
impl<'a> BindingState<'a> {
fn new(input_schema: Option<&'a Schema<'a>>, headers: &'a mut Headers) -> Self {
Self {
headers,
query_params: Vec::new(),
labels: Vec::new(),
input_schema,
raw_payload: None,
visited_bound_members: VisitedMembers::new(),
}
}
fn should_route_binding(&mut self, schema: &Schema<'_>) -> bool {
schema
.member_index()
.map(|idx| self.visited_bound_members.insert(idx))
.unwrap_or(true)
}
fn resolve_member<'s>(&self, schema: &'s Schema<'s>) -> &'s Schema<'s>
where
'a: 's,
{
if let (Some(input_schema), Some(idx)) = (self.input_schema, schema.member_index()) {
input_schema.member_schema_by_index(idx).unwrap_or(schema)
} else if let (Some(input_schema), Some(name)) = (self.input_schema, schema.member_name()) {
input_schema.member_schema(name).unwrap_or(schema)
} else {
schema
}
}
fn resolve_to_input_schema(&self, schema: &Schema<'_>) -> Option<&'a Schema<'a>> {
let input_schema = self.input_schema?;
if let Some(idx) = schema.member_index() {
if let Some(s) = input_schema.member_schema_by_index(idx) {
return Some(s);
}
}
if let Some(name) = schema.member_name() {
return input_schema.member_schema(name);
}
None
}
fn query_param_name(
&self,
schema: &Schema<'_>,
query: &crate::traits::HttpQueryTrait<'_>,
) -> Cow<'a, str> {
match self
.resolve_to_input_schema(schema)
.and_then(|resolved| resolved.http_query())
{
Some(resolved_query) => Cow::Borrowed(resolved_query.value()),
None => Cow::Owned(query.value().to_string()),
}
}
}
fn insert_header(
headers: &mut Headers,
name: Cow<'static, str>,
value: String,
member: &Schema<'_>,
location: std::fmt::Arguments<'_>,
) -> Result<(), SerdeError> {
headers
.try_insert(name, value)
.map(|_| ())
.map_err(|err| header_error(member, location, err))
}
#[cold]
fn header_error(
member: &Schema<'_>,
location: std::fmt::Arguments<'_>,
err: aws_smithy_runtime_api::http::HttpError,
) -> SerdeError {
SerdeError::invalid_input(format!(
"cannot serialize {} to {location}: {err}",
member.member_name().unwrap_or("member"),
))
}
pub(crate) static HTTP_HEADER_NAME: crate::extension::SchemaExtensionKey<Option<http::HeaderName>> =
crate::extension::SchemaExtensionKey::new(|member| {
member
.http_header()
.and_then(|header| http::HeaderName::try_from(header.value()).ok())
});
fn insert_bound_header(
headers: &mut Headers,
member: &Schema<'_>,
header: &crate::traits::HttpHeaderTrait<'_>,
value: String,
) -> Result<(), SerdeError> {
match member.extension(&HTTP_HEADER_NAME) {
Some(name) => headers
.try_insert(name.clone(), value)
.map(|_| ())
.map_err(|err| header_error(member, format_args!("header `{}`", header.value()), err)),
None => insert_header(
headers,
header_name(header),
value,
member,
format_args!("header `{}`", header.value()),
),
}
}
fn header_name(header: &crate::traits::HttpHeaderTrait<'_>) -> Cow<'static, str> {
match header.value_static() {
Some(name) => Cow::Borrowed(name),
None => Cow::Owned(header.value().to_string()),
}
}
struct BindingRouter<'s, 'b, 'a> {
state: &'s mut BindingState<'a>,
body: &'b mut dyn ShapeSerializer,
}
struct Proxy<'p, 'a> {
state: Cell<Option<&'p mut BindingState<'a>>>,
value: &'p dyn SerializableStruct,
}
impl<'p, 'a> SerializableStruct for Proxy<'p, 'a> {
fn schema(&self) -> &Schema<'_> {
self.value.schema()
}
fn serialize_members(&self, serializer: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
let state = self.state.take().ok_or_else(|| {
SerdeError::custom(
"HTTP binding state is already borrowed: the body codec re-entered \
serialize_members on the same struct before the previous call returned",
)
})?;
let mut router = BindingRouter {
state,
body: serializer,
};
let result = self.value.serialize_members(&mut router);
self.state.set(Some(router.state));
result
}
}
impl<'s, 'b, 'a> ShapeSerializer for BindingRouter<'s, 'b, 'a> {
fn write_struct(
&mut self,
schema: &Schema<'_>,
value: &dyn SerializableStruct,
) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
self.body.write_struct(schema, value)
}
fn write_list(
&mut self,
schema: &Schema<'_>,
write_elements: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(header) = schema.http_header() {
if !self.state.should_route_binding(schema) {
return Ok(());
}
let mut collector = ListElementCollector::for_header();
write_elements(&mut collector)?;
let header_val = collector
.values
.iter()
.zip(collector.quotable.iter())
.map(|(s, "able)| {
if quotable {
aws_smithy_runtime_api::http::header_parse::quote_header_value(s.as_str())
.into_owned()
} else {
s.clone()
}
})
.collect::<Vec<_>>()
.join(", ");
insert_bound_header(self.state.headers, schema, header, header_val)?;
return Ok(());
}
if let Some(query) = schema.http_query() {
if !self.state.should_route_binding(schema) {
return Ok(());
}
let mut collector = ListElementCollector::for_query();
write_elements(&mut collector)?;
let name = self.state.query_param_name(schema, query);
for val in collector.values {
self.state.query_params.push((name.clone(), val));
}
return Ok(());
}
self.body.write_list(schema, write_elements)
}
fn write_map(
&mut self,
schema: &Schema<'_>,
write_entries: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(prefix) = schema.http_prefix_headers() {
if !self.state.should_route_binding(schema) {
return Ok(());
}
let mut collector = MapEntryCollector::new(prefix.value().to_string());
write_entries(&mut collector)?;
for (k, v) in collector.entries {
insert_header(
self.state.headers,
Cow::Owned(k),
v,
schema,
format_args!("headers with prefix `{}`", prefix.value()),
)?;
}
return Ok(());
}
if schema.http_query_params().is_some() {
if !self.state.should_route_binding(schema) {
return Ok(());
}
let mut collector = MapEntryCollector::new(String::new());
write_entries(&mut collector)?;
let explicit_query_keys: Vec<&str> = self
.state
.input_schema
.map(|s| {
s.members()
.iter()
.filter_map(|m| m.http_query().map(|q| q.value()))
.collect()
})
.unwrap_or_default();
for (k, v) in collector.entries {
if !explicit_query_keys.contains(&k.as_str()) {
self.state.query_params.push((Cow::Owned(k), v));
}
}
return Ok(());
}
self.body.write_map(schema, write_entries)
}
fn write_boolean(&mut self, schema: &Schema<'_>, value: bool) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(binding) = http_string_binding(schema) {
return self.state.add_binding(binding, schema, &value.to_string());
}
self.body.write_boolean(schema, value)
}
fn write_byte(&mut self, schema: &Schema<'_>, value: i8) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(binding) = http_string_binding(schema) {
return self.state.add_binding(binding, schema, &value.to_string());
}
self.body.write_byte(schema, value)
}
fn write_short(&mut self, schema: &Schema<'_>, value: i16) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(binding) = http_string_binding(schema) {
return self.state.add_binding(binding, schema, &value.to_string());
}
self.body.write_short(schema, value)
}
fn write_integer(&mut self, schema: &Schema<'_>, value: i32) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(binding) = http_string_binding(schema) {
return self.state.add_binding(binding, schema, &value.to_string());
}
self.body.write_integer(schema, value)
}
fn write_long(&mut self, schema: &Schema<'_>, value: i64) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(binding) = http_string_binding(schema) {
return self.state.add_binding(binding, schema, &value.to_string());
}
self.body.write_long(schema, value)
}
fn write_float(&mut self, schema: &Schema<'_>, value: f32) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(binding) = http_string_binding(schema) {
return self
.state
.add_binding(binding, schema, &format_float_f32(value));
}
self.body.write_float(schema, value)
}
fn write_double(&mut self, schema: &Schema<'_>, value: f64) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(binding) = http_string_binding(schema) {
return self
.state
.add_binding(binding, schema, &format_float_f64(value));
}
self.body.write_double(schema, value)
}
fn write_big_integer(
&mut self,
schema: &Schema<'_>,
value: &aws_smithy_types::BigInteger,
) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(binding) = http_string_binding(schema) {
return self.state.add_binding(binding, schema, value.as_ref());
}
self.body.write_big_integer(schema, value)
}
fn write_big_decimal(
&mut self,
schema: &Schema<'_>,
value: &aws_smithy_types::BigDecimal,
) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(binding) = http_string_binding(schema) {
return self.state.add_binding(binding, schema, value.as_ref());
}
self.body.write_big_decimal(schema, value)
}
fn write_string(&mut self, schema: &Schema<'_>, value: &str) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(binding) = http_string_binding(schema) {
if schema.media_type().is_some() {
let encoded = aws_smithy_types::base64::encode(value.as_bytes());
return self.state.add_binding(binding, schema, &encoded);
}
return self.state.add_binding(binding, schema, value);
}
if schema.http_payload().is_some() {
if !self.state.should_route_binding(schema) {
return Ok(());
}
self.state.raw_payload = Some(aws_smithy_types::Blob::new(value));
return Ok(());
}
self.body.write_string(schema, value)
}
fn write_blob(
&mut self,
schema: &Schema<'_>,
value: aws_smithy_types::Blob,
) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if schema.http_header().is_some() {
if !self.state.should_route_binding(schema) {
return Ok(());
}
let encoded = aws_smithy_types::base64::encode(value.as_ref());
let header = schema.http_header().unwrap();
insert_bound_header(self.state.headers, schema, header, encoded)?;
return Ok(());
}
if schema.http_payload().is_some() {
if !self.state.should_route_binding(schema) {
return Ok(());
}
self.state.raw_payload = Some(value);
return Ok(());
}
self.body.write_blob(schema, value)
}
fn write_timestamp(
&mut self,
schema: &Schema<'_>,
value: &aws_smithy_types::DateTime,
) -> Result<(), SerdeError> {
let schema = self.state.resolve_member(schema);
if let Some(binding) = http_string_binding(schema) {
let format = super::timestamp_format_or(
schema,
match binding {
HttpBinding::Header => aws_smithy_types::date_time::Format::HttpDate,
_ => aws_smithy_types::date_time::Format::DateTime,
},
);
let formatted = value
.fmt(format)
.map_err(|e| SerdeError::custom(format!("failed to format timestamp: {e}")))?;
return self.state.add_binding(binding, schema, &formatted);
}
self.body.write_timestamp(schema, value)
}
fn write_document(
&mut self,
schema: &Schema<'_>,
value: &aws_smithy_types::Document,
) -> Result<(), SerdeError> {
self.body.write_document(schema, value)
}
fn write_null(&mut self, schema: &Schema<'_>) -> Result<(), SerdeError> {
self.body.write_null(schema)
}
}
enum HttpBinding {
Header,
Query,
Label,
}
fn http_string_binding(schema: &Schema<'_>) -> Option<HttpBinding> {
if schema.http_header().is_some() {
return Some(HttpBinding::Header);
}
if schema.http_query().is_some() {
return Some(HttpBinding::Query);
}
if schema.http_label().is_some() {
return Some(HttpBinding::Label);
}
None
}
impl<'a> BindingState<'a> {
fn add_binding(
&mut self,
binding: HttpBinding,
schema: &Schema<'_>,
value: &str,
) -> Result<(), SerdeError> {
if !self.should_route_binding(schema) {
return Ok(());
}
match binding {
HttpBinding::Header => {
if let Some(header) = schema.http_header() {
insert_bound_header(self.headers, schema, header, value.to_string())?;
}
}
HttpBinding::Query => {
if let Some(query) = schema.http_query() {
let name = self.query_param_name(schema, query);
self.query_params.push((name, value.to_string()));
}
}
HttpBinding::Label => {
let cow_name = if let Some(resolved) = self.resolve_to_input_schema(schema) {
let name = resolved
.member_name()
.ok_or_else(|| SerdeError::custom("httpLabel on non-member schema"))?;
Cow::Borrowed(name)
} else {
let name = schema
.member_name()
.ok_or_else(|| SerdeError::custom("httpLabel on non-member schema"))?;
Cow::Owned(name.to_string())
};
self.labels.push((cow_name, value.to_string()));
}
}
Ok(())
}
}
macro_rules! noop_writes {
($($method:ident($($arg:ty),*)),+ $(,)?) => {
$(
fn $method(&mut self, _: &Schema<'_>, $(_: $arg),*) -> Result<(), SerdeError> {
Ok(())
}
)+
};
}
#[derive(Copy, Clone)]
enum HttpListTarget {
Header,
Query,
}
struct ListElementCollector {
values: Vec<String>,
quotable: Vec<bool>,
target: HttpListTarget,
}
impl ListElementCollector {
fn for_header() -> Self {
Self::new(HttpListTarget::Header)
}
fn for_query() -> Self {
Self::new(HttpListTarget::Query)
}
fn new(target: HttpListTarget) -> Self {
Self {
values: Vec::new(),
quotable: Vec::new(),
target,
}
}
fn push(&mut self, value: String) {
self.quotable.push(true);
self.values.push(value);
}
fn push_unquotable(&mut self, value: String) {
self.quotable.push(false);
self.values.push(value);
}
}
impl ShapeSerializer for ListElementCollector {
fn write_string(&mut self, _schema: &Schema<'_>, value: &str) -> Result<(), SerdeError> {
self.push(value.to_string());
Ok(())
}
fn write_boolean(&mut self, _: &Schema<'_>, value: bool) -> Result<(), SerdeError> {
self.push(value.to_string());
Ok(())
}
fn write_byte(&mut self, _: &Schema<'_>, value: i8) -> Result<(), SerdeError> {
self.push(value.to_string());
Ok(())
}
fn write_short(&mut self, _: &Schema<'_>, value: i16) -> Result<(), SerdeError> {
self.push(value.to_string());
Ok(())
}
fn write_integer(&mut self, _: &Schema<'_>, value: i32) -> Result<(), SerdeError> {
self.push(value.to_string());
Ok(())
}
fn write_long(&mut self, _: &Schema<'_>, value: i64) -> Result<(), SerdeError> {
self.push(value.to_string());
Ok(())
}
fn write_float(&mut self, _: &Schema<'_>, value: f32) -> Result<(), SerdeError> {
self.push(format_float_f32(value));
Ok(())
}
fn write_double(&mut self, _: &Schema<'_>, value: f64) -> Result<(), SerdeError> {
self.push(format_float_f64(value));
Ok(())
}
fn write_timestamp(
&mut self,
schema: &Schema<'_>,
value: &aws_smithy_types::DateTime,
) -> Result<(), SerdeError> {
let format = super::timestamp_format_or(
schema,
match self.target {
HttpListTarget::Header => aws_smithy_types::date_time::Format::HttpDate,
HttpListTarget::Query => aws_smithy_types::date_time::Format::DateTime,
},
);
self.push_unquotable(
value
.fmt(format)
.map_err(|e| SerdeError::custom(format!("failed to format timestamp: {e}")))?,
);
Ok(())
}
fn write_blob(
&mut self,
_schema: &Schema<'_>,
value: aws_smithy_types::Blob,
) -> Result<(), SerdeError> {
self.push(aws_smithy_types::base64::encode(value.as_ref()));
Ok(())
}
noop_writes! {
write_struct(&dyn SerializableStruct),
write_list(&dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>),
write_map(&dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>),
write_big_integer(&aws_smithy_types::BigInteger),
write_big_decimal(&aws_smithy_types::BigDecimal),
write_document(&aws_smithy_types::Document),
write_null(),
}
}
fn format_float_f32(value: f32) -> String {
if value.is_infinite() {
if value.is_sign_positive() {
"Infinity".to_string()
} else {
"-Infinity".to_string()
}
} else if value.is_nan() {
"NaN".to_string()
} else {
value.to_string()
}
}
fn format_float_f64(value: f64) -> String {
if value.is_infinite() {
if value.is_sign_positive() {
"Infinity".to_string()
} else {
"-Infinity".to_string()
}
} else if value.is_nan() {
"NaN".to_string()
} else {
value.to_string()
}
}
struct MapEntryCollector {
prefix: String,
entries: Vec<(String, String)>,
pending_key: Option<String>,
}
impl MapEntryCollector {
fn new(prefix: String) -> Self {
Self {
prefix,
entries: Vec::new(),
pending_key: None,
}
}
}
impl ShapeSerializer for MapEntryCollector {
fn write_string(&mut self, _schema: &Schema<'_>, value: &str) -> Result<(), SerdeError> {
if let Some(key) = self.pending_key.take() {
self.entries
.push((format!("{}{}", self.prefix, key), value.to_string()));
} else {
self.pending_key = Some(value.to_string());
}
Ok(())
}
fn write_list(
&mut self,
_: &Schema<'_>,
write_elements: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
if let Some(key) = self.pending_key.take() {
let mut collector = ListElementCollector::for_query(); write_elements(&mut collector)?;
for val in collector.values {
self.entries.push((format!("{}{}", self.prefix, key), val));
}
}
Ok(())
}
noop_writes! {
write_struct(&dyn SerializableStruct),
write_map(&dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>),
write_boolean(bool),
write_byte(i8),
write_short(i16),
write_integer(i32),
write_long(i64),
write_float(f32),
write_double(f64),
write_big_integer(&aws_smithy_types::BigInteger),
write_big_decimal(&aws_smithy_types::BigDecimal),
write_blob(aws_smithy_types::Blob),
write_timestamp(&aws_smithy_types::DateTime),
write_document(&aws_smithy_types::Document),
write_null(),
}
}
impl<C> ClientProtocolInner for HttpBindingProtocol<C>
where
C: Codec + Send + Sync + std::fmt::Debug + 'static,
for<'a> C::Deserializer<'a>: ShapeDeserializer,
{
type Request = Request;
type Response = Response;
fn protocol_id(&self) -> &ShapeId<'static> {
&self.protocol_id
}
fn serialize_request(
&self,
input: &dyn SerializableStruct,
input_schema: &Schema<'_>,
endpoint: &str,
cfg: &ConfigBag,
) -> Result<Request, SerdeError> {
let body = self.codec.create_serializer();
self.serialize_request_with_body(body, input, input_schema, endpoint, cfg)
}
fn deserialize_response<'a>(
&self,
response: &'a Response,
output_schema: &Schema<'_>,
cfg: &'a ConfigBag,
) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
let body = response.body().bytes().unwrap_or(&[]);
Ok(http_output_deserializer(
self.codec.create_deserializer(body),
response,
output_schema,
cfg,
))
}
fn deserialize_error_response<'a>(
&self,
response: &'a Response,
cfg: &'a ConfigBag,
) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
let body = response.body().bytes().unwrap_or(&[]);
Ok(http_error_deserializer(
self.codec.create_deserializer(body),
response,
cfg,
))
}
fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec> {
Some(&self.codec)
}
fn update_endpoint(
&self,
request: &mut Request,
endpoint: &aws_smithy_types::endpoint::Endpoint,
cfg: &ConfigBag,
) -> Result<(), SerdeError> {
apply_http_endpoint(request, endpoint, cfg)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serde::SerializableStruct;
use crate::{prelude::*, ShapeType};
#[test]
fn visited_members_inline_dedup() {
let mut v = VisitedMembers::new();
assert!(v.insert(3));
assert!(!v.insert(3));
assert!(v.insert(7));
assert!(v.insert(0));
assert!(!v.insert(3));
assert!(!v.insert(7));
assert!(!v.insert(0));
}
#[test]
fn visited_members_spills_to_overflow() {
let mut v = VisitedMembers::new();
let n = VisitedMembers::INLINE_CAPACITY;
for i in 0..n {
assert!(v.insert(i), "fresh inline insert at {i} must return true");
}
for i in 0..n {
assert!(
!v.insert(i),
"duplicate inline insert at {i} must return false"
);
}
assert!(v.insert(n));
assert!(v.insert(n + 5));
assert!(!v.insert(n));
assert!(!v.insert(n + 5));
assert!(!v.insert(0));
}
struct TestSerializer {
output: Vec<u8>,
}
impl FinishSerializer for TestSerializer {
fn finish(self) -> Vec<u8> {
self.output
}
}
impl ShapeSerializer for TestSerializer {
fn write_struct(
&mut self,
_: &Schema<'_>,
value: &dyn SerializableStruct,
) -> Result<(), SerdeError> {
self.output.push(b'{');
value.serialize_members(self)?;
self.output.push(b'}');
Ok(())
}
fn write_list(
&mut self,
_: &Schema<'_>,
_: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
Ok(())
}
fn write_map(
&mut self,
_: &Schema<'_>,
_: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
Ok(())
}
fn write_boolean(&mut self, _: &Schema<'_>, _: bool) -> Result<(), SerdeError> {
Ok(())
}
fn write_byte(&mut self, _: &Schema<'_>, _: i8) -> Result<(), SerdeError> {
Ok(())
}
fn write_short(&mut self, _: &Schema<'_>, _: i16) -> Result<(), SerdeError> {
Ok(())
}
fn write_integer(&mut self, _: &Schema<'_>, _: i32) -> Result<(), SerdeError> {
Ok(())
}
fn write_long(&mut self, _: &Schema<'_>, _: i64) -> Result<(), SerdeError> {
Ok(())
}
fn write_float(&mut self, _: &Schema<'_>, _: f32) -> Result<(), SerdeError> {
Ok(())
}
fn write_double(&mut self, _: &Schema<'_>, _: f64) -> Result<(), SerdeError> {
Ok(())
}
fn write_big_integer(
&mut self,
_: &Schema<'_>,
_: &aws_smithy_types::BigInteger,
) -> Result<(), SerdeError> {
Ok(())
}
fn write_big_decimal(
&mut self,
_: &Schema<'_>,
_: &aws_smithy_types::BigDecimal,
) -> Result<(), SerdeError> {
Ok(())
}
fn write_string(&mut self, _: &Schema<'_>, v: &str) -> Result<(), SerdeError> {
self.output.extend_from_slice(v.as_bytes());
Ok(())
}
fn write_blob(
&mut self,
_: &Schema<'_>,
_: aws_smithy_types::Blob,
) -> Result<(), SerdeError> {
Ok(())
}
fn write_timestamp(
&mut self,
_: &Schema<'_>,
_: &aws_smithy_types::DateTime,
) -> Result<(), SerdeError> {
Ok(())
}
fn write_document(
&mut self,
_: &Schema<'_>,
_: &aws_smithy_types::Document,
) -> Result<(), SerdeError> {
Ok(())
}
fn write_null(&mut self, _: &Schema<'_>) -> Result<(), SerdeError> {
Ok(())
}
}
struct TestDeserializer<'a> {
input: &'a [u8],
}
impl ShapeDeserializer for TestDeserializer<'_> {
fn read_struct(
&mut self,
_: &Schema<'_>,
_: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
if self.input.is_empty() {
return Err(SerdeError::invalid_input("expected a body, found none"));
}
Ok(())
}
fn read_list(
&mut self,
_: &Schema<'_>,
_: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
Ok(())
}
fn read_map(
&mut self,
_: &Schema<'_>,
_: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
Ok(())
}
fn read_boolean(&mut self, _: &Schema<'_>) -> Result<bool, SerdeError> {
Ok(false)
}
fn read_byte(&mut self, _: &Schema<'_>) -> Result<i8, SerdeError> {
Ok(0)
}
fn read_short(&mut self, _: &Schema<'_>) -> Result<i16, SerdeError> {
Ok(0)
}
fn read_integer(&mut self, _: &Schema<'_>) -> Result<i32, SerdeError> {
Ok(0)
}
fn read_long(&mut self, _: &Schema<'_>) -> Result<i64, SerdeError> {
Ok(0)
}
fn read_float(&mut self, _: &Schema<'_>) -> Result<f32, SerdeError> {
Ok(0.0)
}
fn read_double(&mut self, _: &Schema<'_>) -> Result<f64, SerdeError> {
Ok(0.0)
}
fn read_big_integer(
&mut self,
_: &Schema<'_>,
) -> Result<aws_smithy_types::BigInteger, SerdeError> {
use std::str::FromStr;
Ok(aws_smithy_types::BigInteger::from_str("0").unwrap())
}
fn read_big_decimal(
&mut self,
_: &Schema<'_>,
) -> Result<aws_smithy_types::BigDecimal, SerdeError> {
use std::str::FromStr;
Ok(aws_smithy_types::BigDecimal::from_str("0").unwrap())
}
fn read_string(&mut self, _: &Schema<'_>) -> Result<String, SerdeError> {
Ok(String::from_utf8_lossy(self.input).into_owned())
}
fn read_blob(&mut self, _: &Schema<'_>) -> Result<aws_smithy_types::Blob, SerdeError> {
Ok(aws_smithy_types::Blob::new(vec![]))
}
fn read_timestamp(
&mut self,
_: &Schema<'_>,
) -> Result<aws_smithy_types::DateTime, SerdeError> {
Ok(aws_smithy_types::DateTime::from_secs(0))
}
fn read_document(
&mut self,
_: &Schema<'_>,
) -> Result<aws_smithy_types::Document, SerdeError> {
Ok(aws_smithy_types::Document::Null)
}
fn is_null(&self) -> bool {
false
}
fn container_size(&self) -> Option<usize> {
None
}
}
#[derive(Debug)]
struct TestCodec;
impl Codec for TestCodec {
type Serializer = TestSerializer;
type Deserializer<'a> = TestDeserializer<'a>;
fn create_serializer(&self) -> Self::Serializer {
TestSerializer { output: Vec::new() }
}
fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
TestDeserializer { input }
}
}
static TEST_SCHEMA: Schema<'static> =
Schema::new(crate::shape_id!("test", "TestStruct"), ShapeType::Structure);
struct EmptyStruct;
impl SerializableStruct for EmptyStruct {
fn schema(&self) -> &Schema<'_> {
&TEST_SCHEMA
}
fn serialize_members(&self, _: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
Ok(())
}
}
static NAME_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "TestStruct"),
ShapeType::String,
"name",
0,
);
static MEMBERS: &[&Schema<'_>] = &[&NAME_MEMBER];
static STRUCT_WITH_MEMBER: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "TestStruct"),
ShapeType::Structure,
MEMBERS,
);
struct NameStruct;
impl SerializableStruct for NameStruct {
fn schema(&self) -> &Schema<'_> {
&STRUCT_WITH_MEMBER
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&NAME_MEMBER, "Alice")
}
}
fn make_protocol() -> HttpBindingProtocol<TestCodec> {
HttpBindingProtocol::new(
crate::shape_id!("test", "proto"),
TestCodec,
"application/test",
)
}
#[test]
fn serialize_request_ignores_a_route_computed_for_another_protocol() {
static HTTP_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "GetStatsRequest"),
ShapeType::Structure,
&[],
)
.with_http(crate::traits::HttpTrait::new("PUT", "/stats", None));
for foreign_route in [
"/service/Svc/operation/GetStats",
"/",
] {
let request = make_protocol()
.serialize_request(
&EmptyStruct,
&HTTP_SCHEMA,
foreign_route,
&ConfigBag::base(),
)
.unwrap();
assert_eq!(
"/stats",
request.uri(),
"the `@http` template is authoritative; the route {foreign_route} was computed for \
another protocol and must be ignored",
);
assert_eq!("PUT", request.method());
}
}
#[test]
fn serialize_request_uses_endpoint_as_template_without_an_http_trait() {
let request = make_protocol()
.serialize_request(&EmptyStruct, &TEST_SCHEMA, "/some/path", &ConfigBag::base())
.unwrap();
assert_eq!("/some/path", request.uri());
}
#[test]
fn serialize_sets_content_type() {
let request = make_protocol()
.serialize_request(
&EmptyStruct,
&STRUCT_WITH_MEMBER,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(
request.headers().get("Content-Type").unwrap(),
"application/test"
);
}
#[test]
fn serialize_no_body_members_omits_content_type() {
let request = make_protocol()
.serialize_request(
&EmptyStruct,
&TEST_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert!(request.headers().get("Content-Type").is_none());
}
#[test]
fn presigning_omit_settings_suppress_default_content_headers() {
use crate::header_omit_settings::{HeaderOmitSettings, SharedHeaderOmitSettings};
use aws_smithy_types::config_bag::Layer;
#[derive(Debug)]
struct OmitBoth;
impl HeaderOmitSettings for OmitBoth {
fn should_omit_default_content_type(&self) -> bool {
true
}
fn should_omit_default_content_length(&self) -> bool {
true
}
}
let mut layer = Layer::new("test_omit");
layer.store_put(SharedHeaderOmitSettings::new(OmitBoth));
let cfg = ConfigBag::of_layers(vec![layer]);
let request = make_protocol()
.serialize_request(
&NameStruct,
&STRUCT_WITH_MEMBER,
"https://example.com",
&cfg,
)
.unwrap();
assert!(
request.headers().get("Content-Type").is_none(),
"presigning omit suppresses default Content-Type"
);
assert!(
request.headers().get("Content-Length").is_none(),
"presigning omit suppresses default Content-Length"
);
}
#[test]
fn default_content_headers_inserted_when_omit_settings_absent() {
let request = make_protocol()
.serialize_request(
&NameStruct,
&STRUCT_WITH_MEMBER,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(
request
.headers()
.get("Content-Type")
.expect("Content-Type set"),
"application/test"
);
assert_eq!(
request
.headers()
.get("Content-Length")
.expect("Content-Length set"),
"7"
);
}
#[test]
fn serialize_skips_body_codec_when_no_body_members() {
use std::sync::atomic::{AtomicUsize, Ordering};
static WRITE_CALLS: AtomicUsize = AtomicUsize::new(0);
struct PanicSerializer;
impl FinishSerializer for PanicSerializer {
fn finish(self) -> Vec<u8> {
panic!("body codec finish() called — short-circuit failed");
}
}
impl ShapeSerializer for PanicSerializer {
fn write_struct(
&mut self,
_: &Schema<'_>,
_: &dyn SerializableStruct,
) -> Result<(), SerdeError> {
WRITE_CALLS.fetch_add(1, Ordering::SeqCst);
panic!("body codec write_struct() called — short-circuit failed");
}
fn write_list(
&mut self,
_: &Schema<'_>,
_: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
panic!("body codec write_list() called");
}
fn write_map(
&mut self,
_: &Schema<'_>,
_: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
panic!("body codec write_map() called");
}
fn write_boolean(&mut self, _: &Schema<'_>, _: bool) -> Result<(), SerdeError> {
panic!("body codec write_boolean() called");
}
fn write_byte(&mut self, _: &Schema<'_>, _: i8) -> Result<(), SerdeError> {
panic!("body codec write_byte() called");
}
fn write_short(&mut self, _: &Schema<'_>, _: i16) -> Result<(), SerdeError> {
panic!("body codec write_short() called");
}
fn write_integer(&mut self, _: &Schema<'_>, _: i32) -> Result<(), SerdeError> {
panic!("body codec write_integer() called");
}
fn write_long(&mut self, _: &Schema<'_>, _: i64) -> Result<(), SerdeError> {
panic!("body codec write_long() called");
}
fn write_float(&mut self, _: &Schema<'_>, _: f32) -> Result<(), SerdeError> {
panic!("body codec write_float() called");
}
fn write_double(&mut self, _: &Schema<'_>, _: f64) -> Result<(), SerdeError> {
panic!("body codec write_double() called");
}
fn write_big_integer(
&mut self,
_: &Schema<'_>,
_: &aws_smithy_types::BigInteger,
) -> Result<(), SerdeError> {
panic!("body codec write_big_integer() called");
}
fn write_big_decimal(
&mut self,
_: &Schema<'_>,
_: &aws_smithy_types::BigDecimal,
) -> Result<(), SerdeError> {
panic!("body codec write_big_decimal() called");
}
fn write_string(&mut self, _: &Schema<'_>, _: &str) -> Result<(), SerdeError> {
panic!("body codec write_string() called");
}
fn write_blob(
&mut self,
_: &Schema<'_>,
_: aws_smithy_types::Blob,
) -> Result<(), SerdeError> {
panic!("body codec write_blob() called");
}
fn write_timestamp(
&mut self,
_: &Schema<'_>,
_: &aws_smithy_types::DateTime,
) -> Result<(), SerdeError> {
panic!("body codec write_timestamp() called");
}
fn write_document(
&mut self,
_: &Schema<'_>,
_: &aws_smithy_types::Document,
) -> Result<(), SerdeError> {
panic!("body codec write_document() called");
}
fn write_null(&mut self, _: &Schema<'_>) -> Result<(), SerdeError> {
panic!("body codec write_null() called");
}
}
#[derive(Debug)]
struct PanicCodec;
impl Codec for PanicCodec {
type Serializer = PanicSerializer;
type Deserializer<'a> = TestDeserializer<'a>;
fn create_serializer(&self) -> Self::Serializer {
PanicSerializer
}
fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
TestDeserializer { input }
}
}
static HEADER_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "HeaderOnlyStruct"),
ShapeType::String,
"x_header",
0,
)
.with_http_header("X-Header");
static HEADER_MEMBERS: &[&Schema<'_>] = &[&HEADER_MEMBER];
static HEADER_ONLY_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "HeaderOnlyStruct"),
ShapeType::Structure,
HEADER_MEMBERS,
)
.with_no_body_members();
struct HeaderOnlyStruct;
impl SerializableStruct for HeaderOnlyStruct {
fn schema(&self) -> &Schema<'_> {
&HEADER_ONLY_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&HEADER_MEMBER, "hello")
}
}
let protocol = HttpBindingProtocol::new(
crate::shape_id!("test", "testProtocol"),
PanicCodec,
"application/test",
);
let request = protocol
.serialize_request(
&HeaderOnlyStruct,
&HEADER_ONLY_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.body().bytes().unwrap_or(&[]), b"");
assert!(request.headers().get("Content-Type").is_none());
assert_eq!(request.headers().get("X-Header").unwrap(), "hello");
assert_eq!(WRITE_CALLS.load(Ordering::SeqCst), 0);
}
static STRUCT_PAYLOAD_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "StructPayloadStruct"),
ShapeType::Structure,
"body",
0,
)
.with_http_payload();
static STRUCT_PAYLOAD_MEMBERS: &[&Schema<'_>] = &[&STRUCT_PAYLOAD_MEMBER];
static STRUCT_PAYLOAD_UNKNOWN: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "StructPayloadStruct"),
ShapeType::Structure,
STRUCT_PAYLOAD_MEMBERS,
);
static STRUCT_PAYLOAD_HINTED: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "StructPayloadStruct"),
ShapeType::Structure,
STRUCT_PAYLOAD_MEMBERS,
)
.with_payload_hint(crate::PayloadHint::StructPayload);
static STRUCT_PAYLOAD_MIS_HINTED: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "StructPayloadStruct"),
ShapeType::Structure,
STRUCT_PAYLOAD_MEMBERS,
)
.with_payload_hint(crate::PayloadHint::NoStructPayload);
struct StructPayloadStruct;
impl SerializableStruct for StructPayloadStruct {
fn schema(&self) -> &Schema<'_> {
&STRUCT_PAYLOAD_UNKNOWN
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_struct(&STRUCT_PAYLOAD_MEMBER, &NameStruct)
}
}
fn body_for(schema: &Schema<'_>, input: &dyn SerializableStruct) -> Vec<u8> {
make_protocol()
.serialize_request(input, schema, "https://example.com", &ConfigBag::base())
.unwrap()
.body()
.bytes()
.unwrap_or(&[])
.to_vec()
}
#[test]
fn struct_payload_hint_agrees_with_derived_scan() {
let derived = body_for(&STRUCT_PAYLOAD_UNKNOWN, &StructPayloadStruct);
let hinted = body_for(&STRUCT_PAYLOAD_HINTED, &StructPayloadStruct);
assert_eq!(derived, b"{Alice}");
assert_eq!(hinted, derived);
}
#[test]
fn no_struct_payload_hint_agrees_with_derived_scan() {
static HINTED: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "TestStruct"),
ShapeType::Structure,
MEMBERS,
)
.with_payload_hint(crate::PayloadHint::NoStructPayload);
let derived = body_for(&STRUCT_WITH_MEMBER, &NameStruct);
let hinted = body_for(&HINTED, &NameStruct);
assert_eq!(derived, b"{Alice}");
assert_eq!(hinted, derived);
}
#[test]
fn wrong_struct_payload_hint_changes_request() {
assert_eq!(
body_for(&STRUCT_PAYLOAD_MIS_HINTED, &StructPayloadStruct),
b"",
"a mis-recorded hint must be observable, otherwise the agreement \
tests above prove nothing"
);
}
#[test]
fn payload_hint_defaults_to_unknown() {
assert_eq!(
STRUCT_PAYLOAD_UNKNOWN.payload_hint(),
crate::PayloadHint::Unknown
);
assert_eq!(TEST_SCHEMA.payload_hint(), crate::PayloadHint::Unknown);
}
#[test]
fn serialize_invokes_body_codec_when_has_body_members() {
let request = make_protocol()
.serialize_request(
&NameStruct,
&STRUCT_WITH_MEMBER,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.body().bytes().unwrap(), b"{Alice}");
assert_eq!(
request.headers().get("Content-Type").unwrap(),
"application/test"
);
}
#[test]
fn serialize_sets_uri() {
let request = make_protocol()
.serialize_request(
&EmptyStruct,
&TEST_SCHEMA,
"https://example.com/path",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.uri(), "https://example.com/path");
}
#[test]
fn serialize_body() {
let request = make_protocol()
.serialize_request(
&NameStruct,
&STRUCT_WITH_MEMBER,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.body().bytes().unwrap(), b"{Alice}");
}
static BLOB_PAYLOAD_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "PayloadStruct"),
ShapeType::Blob,
"data",
0,
)
.with_http_payload();
static BLOB_PAYLOAD_MEMBERS: &[&Schema<'_>] = &[&BLOB_PAYLOAD_MEMBER];
static BLOB_PAYLOAD_STRUCT: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "PayloadStruct"),
ShapeType::Structure,
BLOB_PAYLOAD_MEMBERS,
);
static STRING_PAYLOAD_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "PayloadStruct"),
ShapeType::String,
"data",
0,
)
.with_http_payload();
static STRING_PAYLOAD_MEMBERS: &[&Schema<'_>] = &[&STRING_PAYLOAD_MEMBER];
static STRING_PAYLOAD_STRUCT: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "PayloadStruct"),
ShapeType::Structure,
STRING_PAYLOAD_MEMBERS,
);
struct LocallyComputedPayload {
blob: bool,
}
impl SerializableStruct for LocallyComputedPayload {
fn schema(&self) -> &Schema<'_> {
&STRING_PAYLOAD_STRUCT
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
if self.blob {
let computed: Vec<u8> = (0u8..64).collect();
s.write_blob(&BLOB_PAYLOAD_MEMBER, aws_smithy_types::Blob::new(computed))
} else {
let computed: String = (0..16).map(|i| char::from(b'a' + (i % 26) as u8)).collect();
s.write_string(&STRING_PAYLOAD_MEMBER, &computed)
}
}
}
#[test]
fn http_payload_from_a_local_does_not_dangle() {
let request = make_protocol()
.serialize_request(
&LocallyComputedPayload { blob: true },
&BLOB_PAYLOAD_STRUCT,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
let expected: Vec<u8> = (0u8..64).collect();
assert_eq!(request.body().bytes().unwrap(), &expected[..]);
let request = make_protocol()
.serialize_request(
&LocallyComputedPayload { blob: false },
&STRING_PAYLOAD_STRUCT,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.body().bytes().unwrap(), b"abcdefghijklmnop");
}
#[test]
fn blob_payload_reaches_the_body_without_copying() {
struct OwnedBlobPayload(aws_smithy_types::Blob);
impl SerializableStruct for OwnedBlobPayload {
fn schema(&self) -> &Schema<'_> {
&BLOB_PAYLOAD_STRUCT
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_blob(&BLOB_PAYLOAD_MEMBER, self.0.clone())
}
}
let payload = aws_smithy_types::Blob::new(vec![7u8; 4096]);
let src_ptr = payload.as_ref().as_ptr();
let request = make_protocol()
.serialize_request(
&OwnedBlobPayload(payload),
&BLOB_PAYLOAD_STRUCT,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
let body = request.body().bytes().expect("payload body is in memory");
assert_eq!(body.len(), 4096);
assert_eq!(
body.as_ptr(),
src_ptr,
"blob payload was copied; `write_blob` should move the `Bytes` handle all the way \
into `SdkBody`"
);
}
#[test]
fn deserialize_response() {
let response = Response::new(
200u16.try_into().unwrap(),
SdkBody::from(r#"{"name":"Bob"}"#),
);
let base_cfg = ConfigBag::base();
let mut deser = make_protocol()
.deserialize_response(&response, &TEST_SCHEMA, &base_cfg)
.unwrap();
assert_eq!(deser.read_string(&STRING).unwrap(), r#"{"name":"Bob"}"#);
}
#[test]
fn update_endpoint() {
let mut request = make_protocol()
.serialize_request(
&EmptyStruct,
&TEST_SCHEMA,
"https://old.example.com",
&ConfigBag::base(),
)
.unwrap();
let endpoint = aws_smithy_types::endpoint::Endpoint::builder()
.url("https://new.example.com")
.build();
make_protocol()
.update_endpoint(&mut request, &endpoint, &ConfigBag::base())
.unwrap();
assert_eq!(request.uri(), "https://new.example.com/");
}
#[test]
fn protocol_id() {
let protocol = HttpBindingProtocol::new(
crate::shape_id!("aws.protocols", "restJson1"),
TestCodec,
"application/json",
);
assert_eq!(protocol.protocol_id().as_str(), "aws.protocols#restJson1");
}
#[test]
fn invalid_uri_returns_error() {
assert!(make_protocol()
.serialize_request(
&EmptyStruct,
&TEST_SCHEMA,
"not a valid uri\n\n",
&ConfigBag::base()
)
.is_err());
}
static HEADER_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "S"),
ShapeType::String,
"xToken",
0,
)
.with_http_header("X-Token");
static HEADER_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&HEADER_MEMBER],
);
struct HeaderStruct;
impl SerializableStruct for HeaderStruct {
fn schema(&self) -> &Schema<'_> {
&HEADER_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&HEADER_MEMBER, "my-token-value")
}
}
#[test]
fn http_header_string() {
let request = make_protocol()
.serialize_request(
&HeaderStruct,
&HEADER_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.headers().get("X-Token").unwrap(), "my-token-value");
}
#[test]
fn http_header_string_list_quoting_matches_legacy() {
static LIST_MEMBER: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "L"), ShapeType::List, "items", 0)
.with_http_header("X-Items");
static LIST_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "L"),
ShapeType::Structure,
&[&LIST_MEMBER],
);
struct ListStruct;
impl SerializableStruct for ListStruct {
fn schema(&self) -> &Schema<'_> {
&LIST_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_list(&LIST_MEMBER, &|e| {
for v in ["plain", " a", "b ", "(x)", "c,d", "q\"q"] {
e.write_string(&crate::prelude::STRING, v)?;
}
Ok(())
})
}
}
let request = make_protocol()
.serialize_request(
&ListStruct,
&LIST_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(
request.headers().get("X-Items").unwrap(),
r#"plain, " a", "b ", "(x)", "c,d", "q\"q""#
);
}
#[test]
fn http_header_on_a_runtime_built_schema() {
let arena: Vec<String> = vec![
String::from("token"),
String::from("runtime-value"),
String::from("X-Interned-Token"),
];
let member: Schema<'_> = Schema::new_member(
crate::shape_id!("test", "S"),
ShapeType::String,
&arena[0],
0,
)
.with_http_header(crate::intern_header_name(&arena[2]));
assert_eq!(
member.http_header().unwrap().value_static(),
Some("X-Interned-Token")
);
let members = [&member];
let schema = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&members,
);
struct RuntimeStruct<'a> {
schema: &'a Schema<'a>,
member: &'a Schema<'a>,
value: &'a str,
}
impl SerializableStruct for RuntimeStruct<'_> {
fn schema(&self) -> &Schema<'_> {
self.schema
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(self.member, self.value)
}
}
let input = RuntimeStruct {
schema: &schema,
member: &member,
value: &arena[1],
};
assert!(std::ptr::eq(input.schema(), &schema));
let request = make_protocol()
.serialize_request(&input, &schema, "https://example.com", &ConfigBag::base())
.unwrap();
assert_eq!(
request.headers().get("X-Interned-Token").unwrap(),
"runtime-value"
);
}
static INT_HEADER_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "S"),
ShapeType::Integer,
"retryCount",
0,
)
.with_http_header("X-Retry-Count");
static INT_HEADER_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&INT_HEADER_MEMBER],
);
struct IntHeaderStruct;
impl SerializableStruct for IntHeaderStruct {
fn schema(&self) -> &Schema<'_> {
&INT_HEADER_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_integer(&INT_HEADER_MEMBER, 3)
}
}
#[test]
fn http_header_integer() {
let request = make_protocol()
.serialize_request(
&IntHeaderStruct,
&INT_HEADER_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.headers().get("X-Retry-Count").unwrap(), "3");
}
static BOOL_HEADER_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "S"),
ShapeType::Boolean,
"verbose",
0,
)
.with_http_header("X-Verbose");
static BOOL_HEADER_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&BOOL_HEADER_MEMBER],
);
struct BoolHeaderStruct;
impl SerializableStruct for BoolHeaderStruct {
fn schema(&self) -> &Schema<'_> {
&BOOL_HEADER_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_boolean(&BOOL_HEADER_MEMBER, true)
}
}
#[test]
fn http_header_boolean() {
let request = make_protocol()
.serialize_request(
&BoolHeaderStruct,
&BOOL_HEADER_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.headers().get("X-Verbose").unwrap(), "true");
}
static QUERY_MEMBER: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "color", 0)
.with_http_query("color");
static QUERY_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&QUERY_MEMBER],
);
struct QueryStruct;
impl SerializableStruct for QueryStruct {
fn schema(&self) -> &Schema<'_> {
&QUERY_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&QUERY_MEMBER, "blue")
}
}
#[test]
fn http_query_string() {
let request = make_protocol()
.serialize_request(
&QueryStruct,
&QUERY_SCHEMA,
"https://example.com/things",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.uri(), "https://example.com/things?color=blue");
}
static INT_QUERY_MEMBER: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Integer, "size", 0)
.with_http_query("size");
static INT_QUERY_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&INT_QUERY_MEMBER],
);
struct IntQueryStruct;
impl SerializableStruct for IntQueryStruct {
fn schema(&self) -> &Schema<'_> {
&INT_QUERY_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_integer(&INT_QUERY_MEMBER, 42)
}
}
#[test]
fn http_query_integer() {
let request = make_protocol()
.serialize_request(
&IntQueryStruct,
&INT_QUERY_SCHEMA,
"https://example.com/things",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.uri(), "https://example.com/things?size=42");
}
static Q1: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "a", 0)
.with_http_query("a");
static Q2: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "b", 1)
.with_http_query("b");
static MULTI_QUERY_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&Q1, &Q2],
);
struct MultiQueryStruct;
impl SerializableStruct for MultiQueryStruct {
fn schema(&self) -> &Schema<'_> {
&MULTI_QUERY_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&Q1, "x")?;
s.write_string(&Q2, "y")
}
}
#[test]
fn http_query_multiple_params() {
let request = make_protocol()
.serialize_request(
&MultiQueryStruct,
&MULTI_QUERY_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.uri(), "https://example.com?a=x&b=y");
}
#[test]
fn http_query_percent_encodes_values() {
struct SpaceQueryStruct;
impl SerializableStruct for SpaceQueryStruct {
fn schema(&self) -> &Schema<'_> {
&QUERY_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&QUERY_MEMBER, "hello world")
}
}
let request = make_protocol()
.serialize_request(
&SpaceQueryStruct,
&QUERY_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.uri(), "https://example.com?color=hello%20world");
}
static LABEL_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "S"),
ShapeType::String,
"bucketName",
0,
)
.with_http_label();
static LABEL_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&LABEL_MEMBER],
);
struct LabelStruct;
impl SerializableStruct for LabelStruct {
fn schema(&self) -> &Schema<'_> {
&LABEL_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&LABEL_MEMBER, "my-bucket")
}
}
#[test]
fn http_label_substitution() {
let request = make_protocol()
.serialize_request(
&LabelStruct,
&LABEL_SCHEMA,
"https://example.com/{bucketName}/objects",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.uri(), "https://example.com/my-bucket/objects");
}
#[test]
fn http_label_percent_encodes() {
struct SpecialLabelStruct;
impl SerializableStruct for SpecialLabelStruct {
fn schema(&self) -> &Schema<'_> {
&LABEL_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&LABEL_MEMBER, "my bucket/name")
}
}
let request = make_protocol()
.serialize_request(
&SpecialLabelStruct,
&LABEL_SCHEMA,
"https://example.com/{bucketName}",
&ConfigBag::base(),
)
.unwrap();
assert!(request.uri().contains("my%20bucket%2Fname"));
}
static INT_LABEL_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "S"),
ShapeType::Integer,
"itemId",
0,
)
.with_http_label();
static INT_LABEL_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&INT_LABEL_MEMBER],
);
struct IntLabelStruct;
impl SerializableStruct for IntLabelStruct {
fn schema(&self) -> &Schema<'_> {
&INT_LABEL_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_integer(&INT_LABEL_MEMBER, 123)
}
}
#[test]
fn http_label_integer() {
let request = make_protocol()
.serialize_request(
&IntLabelStruct,
&INT_LABEL_SCHEMA,
"https://example.com/items/{itemId}",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.uri(), "https://example.com/items/123");
}
#[test]
fn missing_or_empty_label_is_an_error() {
fn expand(template: &str, labels: &[(&'static str, &str)]) -> Result<String, String> {
let labels: Vec<(Cow<'_, str>, String)> = labels
.iter()
.map(|(n, v)| (Cow::Borrowed(*n), v.to_string()))
.collect();
let mut out = String::new();
append_uri_with_labels(template, &labels, &mut out)
.map(|()| out)
.map_err(|e| e.to_string())
}
let template = "/{Bucket}/{Key+}";
assert_eq!(
expand(template, &[("Bucket", "b"), ("Key", "a//b")]).unwrap(),
"/b/a//b",
"an empty inner segment of a greedy label is still allowed"
);
for (labels, missing) in [
(&[("Key", "k")][..], "Bucket"),
(&[("Bucket", ""), ("Key", "k")][..], "Bucket"),
(&[("Bucket", "b")][..], "Key"),
(&[("Bucket", "b"), ("Key", "")][..], "Key"),
] {
let err = expand(template, labels).expect_err(&format!("{labels:?}"));
assert!(
err.contains(&format!("{missing} was missing: cannot be empty or unset")),
"{err}"
);
}
assert_eq!(expand("/a{b", &[]).unwrap(), "/a{b");
}
#[test]
fn empty_label_fails_serialize_request() {
static BUCKET: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "Get"),
ShapeType::String,
"Bucket",
0,
)
.with_http_label();
static KEY: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "Get"), ShapeType::String, "Key", 1)
.with_http_label();
static GET_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "Get"),
ShapeType::Structure,
&[&BUCKET, &KEY],
)
.with_http(crate::traits::HttpTrait::new(
"GET",
"/{Bucket}/{Key+}",
None,
));
struct Get(&'static str);
impl SerializableStruct for Get {
fn schema(&self) -> &Schema<'_> {
&GET_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&BUCKET, self.0)?;
s.write_string(&KEY, "my-key")
}
}
let protocol = make_protocol();
let request = protocol
.serialize_request(&Get("b"), &GET_SCHEMA, "", &ConfigBag::base())
.unwrap();
assert_eq!(request.uri(), "/b/my-key");
let err = protocol
.serialize_request(&Get(""), &GET_SCHEMA, "", &ConfigBag::base())
.expect_err("an empty bucket must not produce `//my-key`");
assert!(
err.to_string()
.contains("Bucket was missing: cannot be empty or unset"),
"{err}"
);
}
static COMBINED_LABEL: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "id", 0)
.with_http_label();
static COMBINED_HEADER: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "token", 1)
.with_http_header("X-Token");
static COMBINED_QUERY: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "S"),
ShapeType::String,
"filter",
2,
)
.with_http_query("filter");
static COMBINED_BODY: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "data", 3);
static COMBINED_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[
&COMBINED_LABEL,
&COMBINED_HEADER,
&COMBINED_QUERY,
&COMBINED_BODY,
],
);
struct CombinedStruct;
impl SerializableStruct for CombinedStruct {
fn schema(&self) -> &Schema<'_> {
&COMBINED_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&COMBINED_LABEL, "item-42")?;
s.write_string(&COMBINED_HEADER, "secret")?;
s.write_string(&COMBINED_QUERY, "active")?;
s.write_string(&COMBINED_BODY, "payload-data")
}
}
#[test]
fn combined_bindings() {
let request = make_protocol()
.serialize_request(
&CombinedStruct,
&COMBINED_SCHEMA,
"https://example.com/{id}/details",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(
request.uri(),
"https://example.com/item-42/details?filter=active"
);
assert_eq!(request.headers().get("X-Token").unwrap(), "secret");
let body = request.body().bytes().unwrap();
assert!(body
.windows(b"payload-data".len())
.any(|w| w == b"payload-data"));
}
static PREFIX_MEMBER: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Map, "metadata", 0)
.with_http_prefix_headers("X-Meta-");
static PREFIX_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&PREFIX_MEMBER],
);
struct PrefixHeaderStruct;
impl SerializableStruct for PrefixHeaderStruct {
fn schema(&self) -> &Schema<'_> {
&PREFIX_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_map(&PREFIX_MEMBER, &|s| {
s.write_string(&STRING, "Color")?;
s.write_string(&STRING, "red")?;
s.write_string(&STRING, "Size")?;
s.write_string(&STRING, "large")?;
Ok(())
})
}
}
#[test]
fn http_prefix_headers() {
let request = make_protocol()
.serialize_request(
&PrefixHeaderStruct,
&PREFIX_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.headers().get("X-Meta-Color").unwrap(), "red");
assert_eq!(request.headers().get("X-Meta-Size").unwrap(), "large");
}
fn serialize_prefix_entry(key: &'static str, value: &'static str) -> Result<(), SerdeError> {
struct Entry(&'static str, &'static str);
impl SerializableStruct for Entry {
fn schema(&self) -> &Schema<'_> {
&PREFIX_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_map(&PREFIX_MEMBER, &|s| {
s.write_string(&STRING, self.0)?;
s.write_string(&STRING, self.1)
})
}
}
make_protocol()
.serialize_request(
&Entry(key, value),
&PREFIX_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.map(|_| ())
}
#[test]
fn invalid_prefix_header_name_or_value_is_an_error_not_a_panic() {
for (key, value) in [
("has space", "secret-1"),
("colon:", "secret-2"),
("non-ascii-é", "secret-3"),
("Ok", "secret\nbreak"),
] {
let err = serialize_prefix_entry(key, value)
.expect_err(&format!("{key:?}/{value:?} must be rejected"));
let message = err.to_string();
assert!(
message.contains("metadata") && message.contains("prefix `X-Meta-`"),
"{message}"
);
assert!(!message.contains(value), "value leaked: {message}");
}
serialize_prefix_entry("Ok", "fine").expect("a valid entry still serializes");
}
#[test]
fn invalid_header_value_is_an_error_not_a_panic() {
struct Scalar;
impl SerializableStruct for Scalar {
fn schema(&self) -> &Schema<'_> {
&HEADER_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&HEADER_MEMBER, "secret\r\ninjected: 1")
}
}
static LIST_MEMBER: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "L"), ShapeType::List, "items", 0)
.with_http_header("X-Items");
static LIST_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "L"),
ShapeType::Structure,
&[&LIST_MEMBER],
);
struct List;
impl SerializableStruct for List {
fn schema(&self) -> &Schema<'_> {
&LIST_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_list(&LIST_MEMBER, &|e| {
e.write_string(&STRING, "ok")?;
e.write_string(&STRING, "secret\nvalue")
})
}
}
let protocol = make_protocol();
let cases: [(&dyn SerializableStruct, &Schema<'static>, &str); 2] = [
(&Scalar, &HEADER_SCHEMA, "xToken to header `X-Token`"),
(&List, &LIST_SCHEMA, "items to header `X-Items`"),
];
for (input, schema, expected) in cases {
let err = protocol
.serialize_request(input, schema, "https://example.com", &ConfigBag::base())
.expect_err("an invalid header value must be rejected");
let message = err.to_string();
assert!(message.contains(expected), "{message}");
assert!(!message.contains("secret"), "value leaked: {message}");
}
}
#[test]
fn invalid_modeled_header_name_is_an_error_not_a_panic() {
static BAD_MEMBER: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "B"), ShapeType::String, "bad", 0)
.with_http_header("has space");
static BAD_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "B"),
ShapeType::Structure,
&[&BAD_MEMBER],
);
struct Bad;
impl SerializableStruct for Bad {
fn schema(&self) -> &Schema<'_> {
&BAD_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&BAD_MEMBER, "value")
}
}
let protocol = make_protocol();
for _ in 0..2 {
let err = protocol
.serialize_request(&Bad, &BAD_SCHEMA, "https://example.com", &ConfigBag::base())
.expect_err("an invalid header name must be rejected");
let message = err.to_string();
assert!(message.contains("bad to header `has space`"), "{message}");
}
}
#[test]
fn cached_header_names_are_reused_by_every_header_write() {
static SCALAR: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "H"),
ShapeType::String,
"scalar",
0,
)
.with_http_header("X-Amz-Scalar");
static LIST: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "H"), ShapeType::List, "list", 1)
.with_http_header("x-amz-list");
static BLOB: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "H"), ShapeType::Blob, "blob", 2)
.with_http_header("Content-MD5");
static SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "H"),
ShapeType::Structure,
&[&SCALAR, &LIST, &BLOB],
);
struct Headers3;
impl SerializableStruct for Headers3 {
fn schema(&self) -> &Schema<'_> {
&SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&SCALAR, "one")?;
s.write_list(&LIST, &|e| {
e.write_string(&STRING, "a")?;
e.write_string(&STRING, "b")
})?;
s.write_blob(&BLOB, aws_smithy_types::Blob::new("md5"))
}
}
let protocol = make_protocol();
for _ in 0..2 {
let request = protocol
.serialize_request(
&Headers3,
&SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
let mut headers: Vec<_> = request
.headers()
.iter()
.filter(|(name, _)| !name.starts_with("content-type"))
.collect();
headers.sort();
assert_eq!(
headers,
[
("content-md5", "bWQ1"),
("x-amz-list", "a, b"),
("x-amz-scalar", "one"),
]
);
}
}
static QUERY_PARAMS_MEMBER: Schema<'static> =
Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Map, "params", 0)
.with_http_query_params();
static QUERY_PARAMS_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&QUERY_PARAMS_MEMBER],
);
struct QueryParamsStruct;
impl SerializableStruct for QueryParamsStruct {
fn schema(&self) -> &Schema<'_> {
&QUERY_PARAMS_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_map(&QUERY_PARAMS_MEMBER, &|s| {
s.write_string(&STRING, "page")?;
s.write_string(&STRING, "2")?;
s.write_string(&STRING, "limit")?;
s.write_string(&STRING, "50")?;
Ok(())
})
}
}
#[test]
fn http_query_params() {
let request = make_protocol()
.serialize_request(
&QueryParamsStruct,
&QUERY_PARAMS_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(request.uri(), "https://example.com?page=2&limit=50");
}
static TS_HEADER_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "S"),
ShapeType::Timestamp,
"ifModified",
0,
)
.with_http_header("If-Modified-Since");
static TS_HEADER_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&TS_HEADER_MEMBER],
);
struct TimestampHeaderStruct;
impl SerializableStruct for TimestampHeaderStruct {
fn schema(&self) -> &Schema<'_> {
&TS_HEADER_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_timestamp(&TS_HEADER_MEMBER, &aws_smithy_types::DateTime::from_secs(0))
}
}
#[test]
fn timestamp_header_uses_http_date() {
let request = make_protocol()
.serialize_request(
&TimestampHeaderStruct,
&TS_HEADER_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
let value = request.headers().get("If-Modified-Since").unwrap();
assert!(value.contains("1970"), "expected http-date, got: {value}");
}
static TS_QUERY_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "S"),
ShapeType::Timestamp,
"since",
0,
)
.with_http_query("since");
static TS_QUERY_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&TS_QUERY_MEMBER],
);
struct TimestampQueryStruct;
impl SerializableStruct for TimestampQueryStruct {
fn schema(&self) -> &Schema<'_> {
&TS_QUERY_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_timestamp(&TS_QUERY_MEMBER, &aws_smithy_types::DateTime::from_secs(0))
}
}
#[test]
fn timestamp_query_uses_date_time() {
let request = make_protocol()
.serialize_request(
&TimestampQueryStruct,
&TS_QUERY_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
assert_eq!(
request.uri(),
"https://example.com?since=1970-01-01T00%3A00%3A00Z"
);
}
static BOUND_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "S"),
ShapeType::String,
"headerVal",
0,
)
.with_http_header("X-Val");
static UNBOUND_MEMBER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "S"),
ShapeType::String,
"bodyVal",
1,
);
static MIXED_SCHEMA: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "S"),
ShapeType::Structure,
&[&BOUND_MEMBER, &UNBOUND_MEMBER],
);
struct MixedStruct;
impl SerializableStruct for MixedStruct {
fn schema(&self) -> &Schema<'_> {
&MIXED_SCHEMA
}
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(&BOUND_MEMBER, "in-header")?;
s.write_string(&UNBOUND_MEMBER, "in-body")
}
}
#[test]
fn bound_members_not_in_body() {
let request = make_protocol()
.serialize_request(
&MixedStruct,
&MIXED_SCHEMA,
"https://example.com",
&ConfigBag::base(),
)
.unwrap();
let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
assert!(
body.contains("in-body"),
"body should contain unbound member"
);
assert!(
!body.contains("in-header"),
"body should NOT contain header-bound member"
);
assert_eq!(request.headers().get("X-Val").unwrap(), "in-header");
}
static RESP_NAME: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "Out"),
ShapeType::String,
"name",
0,
)
.with_http_header("x-name");
static RESP_STATUS: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "Out"),
ShapeType::Integer,
"status",
1,
)
.with_http_response_code();
static RESP_BOUND: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "Out"),
ShapeType::Structure,
&[&RESP_NAME, &RESP_STATUS],
);
static RESP_BODY_FIELD: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "Out"),
ShapeType::String,
"note",
2,
);
static RESP_MIXED: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "Out"),
ShapeType::Structure,
&[&RESP_NAME, &RESP_STATUS, &RESP_BODY_FIELD],
);
static RESP_STREAM: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "Stream"),
ShapeType::Blob,
"body",
0,
)
.with_http_payload()
.with_streaming();
static RESP_STREAM_HEADER: Schema<'static> = Schema::new_member(
crate::shape_id!("test", "Stream"),
ShapeType::String,
"name",
1,
)
.with_http_header("x-name");
static RESP_STREAMING_OUT: Schema<'static> = Schema::new_struct(
crate::shape_id!("test", "Stream"),
ShapeType::Structure,
&[&RESP_STREAM, &RESP_STREAM_HEADER],
);
fn response_with(
status: u16,
headers: &[(&str, &str)],
body: aws_smithy_types::body::SdkBody,
) -> Response {
let mut builder = http::Response::builder().status(status);
for (name, value) in headers {
builder = builder.header(*name, *value);
}
Response::try_from(builder.body(body).expect("response")).expect("convertible")
}
fn collect<'c>(
strings: &'c mut Vec<(String, String)>,
integers: &'c mut Vec<(String, i32)>,
) -> impl FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError> + 'c {
move |member, deser| {
let name = member.member_name().unwrap_or("?").to_string();
match member.shape_type() {
ShapeType::Integer => integers.push((name, deser.read_integer(member)?)),
_ => strings.push((name, deser.read_string(member)?)),
}
Ok(())
}
}
#[test]
fn deserialize_response_reads_modeled_headers_and_status() {
let response = response_with(
201,
&[("x-name", "widget")],
aws_smithy_types::body::SdkBody::from("{}"),
);
let mut strings = Vec::new();
let mut integers = Vec::new();
make_protocol()
.deserialize_response(&response, &RESP_BOUND, &ConfigBag::base())
.unwrap()
.read_struct(&RESP_BOUND, &mut collect(&mut strings, &mut integers))
.unwrap();
assert_eq!(strings, vec![("name".to_string(), "widget".to_string())]);
assert_eq!(integers, vec![("status".to_string(), 201)]);
}
#[test]
fn deserialize_error_response_tolerates_an_empty_body_and_still_reads_bindings() {
let response = response_with(
404,
&[("x-name", "missing")],
aws_smithy_types::body::SdkBody::empty(),
);
let mut strings = Vec::new();
let mut integers = Vec::new();
make_protocol()
.deserialize_error_response(&response, &ConfigBag::base())
.unwrap()
.read_struct(&RESP_MIXED, &mut collect(&mut strings, &mut integers))
.expect("an empty error body must not be handed to the codec");
assert_eq!(strings, vec![("name".to_string(), "missing".to_string())]);
assert_eq!(integers, vec![("status".to_string(), 404)]);
}
#[test]
fn deserialize_response_keeps_the_codecs_empty_body_strictness_for_a_success() {
let response = response_with(
200,
&[("x-name", "here")],
aws_smithy_types::body::SdkBody::empty(),
);
let mut strings = Vec::new();
let mut integers = Vec::new();
let err = make_protocol()
.deserialize_response(&response, &RESP_MIXED, &ConfigBag::base())
.unwrap()
.read_struct(&RESP_MIXED, &mut collect(&mut strings, &mut integers))
.expect_err("a successful response still hands an empty body to the codec");
assert!(
format!("{err}").contains("expected a body"),
"unexpected: {err}"
);
}
#[test]
fn deserialize_response_reads_headers_without_consuming_a_streaming_payload() {
let response = response_with(
200,
&[("x-name", "stream")],
aws_smithy_types::body::SdkBody::taken(),
);
assert!(
response.body().bytes().is_none(),
"precondition: the body is not buffered"
);
let mut strings = Vec::new();
let mut integers = Vec::new();
make_protocol()
.deserialize_response(&response, &RESP_STREAMING_OUT, &ConfigBag::base())
.unwrap()
.read_struct(
&RESP_STREAMING_OUT,
&mut collect(&mut strings, &mut integers),
)
.unwrap();
assert_eq!(
strings,
vec![("name".to_string(), "stream".to_string())],
"the header must be read and the streaming payload left untouched"
);
}
#[test]
fn deserialize_response_still_hands_a_body_only_output_to_the_codec() {
let response = response_with(
200,
&[],
aws_smithy_types::body::SdkBody::from(r#"{"name":"Bob"}"#),
);
let base_cfg = ConfigBag::base();
let mut deser = make_protocol()
.deserialize_response(&response, &STRUCT_WITH_MEMBER, &base_cfg)
.unwrap();
assert_eq!(deser.read_string(&STRING).unwrap(), r#"{"name":"Bob"}"#);
}
}