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,
) {
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());
if let Some(v) = value {
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);
}
}
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 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 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 && (s.contains(',') || s.contains('"')) {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
} else {
s.clone()
}
})
.collect::<Vec<_>>()
.join(", ");
self.state.headers.insert(header_name(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 {
self.state.headers.insert(k, v);
}
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());
self.state
.headers
.insert(header_name(schema.http_header().unwrap()), 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 = if let Some(ts_trait) = schema.timestamp_format() {
match ts_trait.format() {
crate::traits::TimestampFormat::EpochSeconds => {
aws_smithy_types::date_time::Format::EpochSeconds
}
crate::traits::TimestampFormat::HttpDate => {
aws_smithy_types::date_time::Format::HttpDate
}
crate::traits::TimestampFormat::DateTime => {
aws_smithy_types::date_time::Format::DateTime
}
}
} else {
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() {
self.headers.insert(header_name(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 = match schema.timestamp_format() {
Some(ts) => match ts.format() {
crate::traits::TimestampFormat::EpochSeconds => {
aws_smithy_types::date_time::Format::EpochSeconds
}
crate::traits::TimestampFormat::HttpDate => {
aws_smithy_types::date_time::Format::HttpDate
}
crate::traits::TimestampFormat::DateTime => {
aws_smithy_types::date_time::Format::DateTime
}
},
None => 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: &ConfigBag,
) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
let body = response.body().bytes().unwrap_or(&[]);
Ok(Box::new(self.codec.create_deserializer(body)))
}
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> {
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 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 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 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 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 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 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 mut deser = make_protocol()
.deserialize_response(&response, &TEST_SCHEMA, &ConfigBag::base())
.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 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_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>(&'a Schema<'a>, &'a str);
impl SerializableStruct for RuntimeStruct<'_> {
fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
s.write_string(self.0, self.1)
}
}
let request = make_protocol()
.serialize_request(
&RuntimeStruct(&member, &arena[1]),
&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 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 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 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 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 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 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 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 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 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");
}
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 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 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");
}
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 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 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 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 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");
}
}