use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer};
use crate::{Schema, ShapeId};
use aws_smithy_types::config_bag::ConfigBag;
use aws_smithy_types::endpoint::Endpoint;
use aws_smithy_types::error::metadata::{Builder as ErrorMetadataBuilder, ErrorMetadata};
pub trait ClientProtocolInner: Send + Sync + std::fmt::Debug {
type Request;
type Response;
fn protocol_id(&self) -> &ShapeId<'static>;
fn serialize_request(
&self,
input: &dyn SerializableStruct,
input_schema: &Schema<'_>,
endpoint: &str,
cfg: &ConfigBag,
) -> Result<Self::Request, SerdeError>;
fn deserialize_response<'a>(
&self,
response: &'a Self::Response,
output_schema: &Schema<'_>,
cfg: &ConfigBag,
) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError>;
fn parse_error_metadata(
&self,
response: &Self::Response,
cfg: &ConfigBag,
) -> Result<ErrorMetadataBuilder, SerdeError> {
let _ = (response, cfg);
Ok(ErrorMetadata::builder())
}
fn deserialize_error_response<'a>(
&self,
response: &'a Self::Response,
cfg: &ConfigBag,
) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
self.deserialize_response(response, &crate::prelude::DOCUMENT, cfg)
}
fn update_endpoint(
&self,
request: &mut Self::Request,
endpoint: &Endpoint,
cfg: &ConfigBag,
) -> Result<(), SerdeError>;
fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec> {
None
}
fn event_stream_media_type(&self) -> Option<&str> {
None
}
fn parse_event_stream_error_metadata(
&self,
payload: &[u8],
) -> Result<ErrorMetadataBuilder, SerdeError> {
let _ = payload;
Ok(ErrorMetadata::builder())
}
}
pub trait ClientProtocol<
Req = aws_smithy_runtime_api::http::Request,
Res = aws_smithy_runtime_api::http::Response,
>: Send + Sync + std::fmt::Debug
{
fn protocol_id(&self) -> &ShapeId<'static>;
fn serialize_request(
&self,
input: &dyn SerializableStruct,
input_schema: &Schema<'_>,
endpoint: &str,
cfg: &ConfigBag,
) -> Result<Req, SerdeError>;
fn deserialize_response<'a>(
&self,
response: &'a Res,
output_schema: &Schema<'_>,
cfg: &ConfigBag,
) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError>;
fn parse_error_metadata(
&self,
response: &Res,
cfg: &ConfigBag,
) -> Result<ErrorMetadataBuilder, SerdeError>;
fn deserialize_error_response<'a>(
&self,
response: &'a Res,
cfg: &ConfigBag,
) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError>;
fn update_endpoint(
&self,
request: &mut Req,
endpoint: &Endpoint,
cfg: &ConfigBag,
) -> Result<(), SerdeError>;
fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec>;
fn event_stream_media_type(&self) -> Option<&str>;
fn parse_event_stream_error_metadata(
&self,
payload: &[u8],
) -> Result<ErrorMetadataBuilder, SerdeError>;
}
impl<P> ClientProtocol<P::Request, P::Response> for P
where
P: ClientProtocolInner,
{
fn protocol_id(&self) -> &ShapeId<'static> {
<Self as ClientProtocolInner>::protocol_id(self)
}
fn serialize_request(
&self,
input: &dyn SerializableStruct,
input_schema: &Schema<'_>,
endpoint: &str,
cfg: &ConfigBag,
) -> Result<P::Request, SerdeError> {
<Self as ClientProtocolInner>::serialize_request(self, input, input_schema, endpoint, cfg)
}
fn deserialize_response<'a>(
&self,
response: &'a P::Response,
output_schema: &Schema<'_>,
cfg: &ConfigBag,
) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
<Self as ClientProtocolInner>::deserialize_response(self, response, output_schema, cfg)
}
fn parse_error_metadata(
&self,
response: &P::Response,
cfg: &ConfigBag,
) -> Result<ErrorMetadataBuilder, SerdeError> {
<Self as ClientProtocolInner>::parse_error_metadata(self, response, cfg)
}
fn deserialize_error_response<'a>(
&self,
response: &'a P::Response,
cfg: &ConfigBag,
) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
<Self as ClientProtocolInner>::deserialize_error_response(self, response, cfg)
}
fn update_endpoint(
&self,
request: &mut P::Request,
endpoint: &Endpoint,
cfg: &ConfigBag,
) -> Result<(), SerdeError> {
<Self as ClientProtocolInner>::update_endpoint(self, request, endpoint, cfg)
}
fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec> {
<Self as ClientProtocolInner>::payload_codec(self)
}
fn event_stream_media_type(&self) -> Option<&str> {
<Self as ClientProtocolInner>::event_stream_media_type(self)
}
fn parse_event_stream_error_metadata(
&self,
payload: &[u8],
) -> Result<ErrorMetadataBuilder, SerdeError> {
<Self as ClientProtocolInner>::parse_event_stream_error_metadata(self, payload)
}
}
pub fn apply_http_endpoint(
request: &mut aws_smithy_runtime_api::http::Request,
endpoint: &Endpoint,
cfg: &ConfigBag,
) -> Result<(), SerdeError> {
use std::borrow::Cow;
let endpoint_prefix = cfg.load::<aws_smithy_runtime_api::client::endpoint::EndpointPrefix>();
let endpoint_url = match endpoint_prefix {
None => Cow::Borrowed(endpoint.url()),
Some(prefix) => {
let parsed: http::Uri = endpoint
.url()
.parse()
.map_err(|e| SerdeError::custom(format!("invalid endpoint URI: {e}")))?;
let scheme = parsed.scheme_str().unwrap_or_default();
let prefix = prefix.as_str();
let authority = parsed.authority().map(|a| a.as_str()).unwrap_or_default();
let path_and_query = parsed
.path_and_query()
.map(|pq| pq.as_str())
.unwrap_or_default();
Cow::Owned(format!("{scheme}://{prefix}{authority}{path_and_query}"))
}
};
request.uri_mut().set_endpoint(&endpoint_url).map_err(|e| {
SerdeError::custom(format!("failed to apply endpoint `{endpoint_url}`: {e}"))
})?;
for (header_name, header_values) in endpoint.headers() {
request.headers_mut().remove(header_name);
for value in header_values {
request
.headers_mut()
.append(header_name.to_owned(), value.to_owned());
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ServiceShapeName(std::borrow::Cow<'static, str>);
impl ServiceShapeName {
pub fn new(name: impl Into<std::borrow::Cow<'static, str>>) -> Self {
Self(name.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl aws_smithy_types::config_bag::Storable for ServiceShapeName {
type Storer = aws_smithy_types::config_bag::StoreReplace<Self>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ServiceShapeNamespace(std::borrow::Cow<'static, str>);
impl ServiceShapeNamespace {
pub fn new(namespace: impl Into<std::borrow::Cow<'static, str>>) -> Self {
Self(namespace.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl aws_smithy_types::config_bag::Storable for ServiceShapeNamespace {
type Storer = aws_smithy_types::config_bag::StoreReplace<Self>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ServiceVersion(std::borrow::Cow<'static, str>);
impl ServiceVersion {
pub fn new(version: impl Into<std::borrow::Cow<'static, str>>) -> Self {
Self(version.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl aws_smithy_types::config_bag::Storable for ServiceVersion {
type Storer = aws_smithy_types::config_bag::StoreReplace<Self>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ServiceXmlNamespace {
uri: std::borrow::Cow<'static, str>,
prefix: Option<std::borrow::Cow<'static, str>>,
}
impl ServiceXmlNamespace {
pub fn new(
uri: impl Into<std::borrow::Cow<'static, str>>,
prefix: Option<std::borrow::Cow<'static, str>>,
) -> Self {
Self {
uri: uri.into(),
prefix,
}
}
pub fn uri(&self) -> &str {
&self.uri
}
pub fn prefix(&self) -> Option<&str> {
self.prefix.as_deref()
}
}
impl aws_smithy_types::config_bag::Storable for ServiceXmlNamespace {
type Storer = aws_smithy_types::config_bag::StoreReplace<Self>;
}
#[derive(Debug)]
pub struct SharedClientProtocol<
Req = aws_smithy_runtime_api::http::Request,
Res = aws_smithy_runtime_api::http::Response,
> {
inner: std::sync::Arc<dyn ClientProtocol<Req, Res>>,
}
impl<Req, Res> Clone for SharedClientProtocol<Req, Res> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<Req, Res> SharedClientProtocol<Req, Res>
where
Req: 'static,
Res: 'static,
{
pub fn new<P>(protocol: P) -> Self
where
P: ClientProtocol<Req, Res> + 'static,
{
Self {
inner: std::sync::Arc::new(protocol),
}
}
}
impl<Req, Res> std::ops::Deref for SharedClientProtocol<Req, Res> {
type Target = dyn ClientProtocol<Req, Res>;
fn deref(&self) -> &Self::Target {
&*self.inner
}
}
impl aws_smithy_types::config_bag::Storable
for SharedClientProtocol<
aws_smithy_runtime_api::http::Request,
aws_smithy_runtime_api::http::Response,
>
{
type Storer = aws_smithy_types::config_bag::StoreReplace<Self>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer};
use crate::{Schema, ShapeId};
use aws_smithy_runtime_api::http::{Request, Response, StatusCode};
use aws_smithy_types::body::SdkBody;
use aws_smithy_types::config_bag::{ConfigBag, Layer};
use aws_smithy_types::endpoint::Endpoint;
#[derive(Debug)]
struct StubProtocol;
static STUB_ID: ShapeId<'static> =
ShapeId::from_parts("test#StubProtocol", "test", "StubProtocol");
impl ClientProtocolInner for StubProtocol {
type Request = Request;
type Response = Response;
fn protocol_id(&self) -> &ShapeId<'static> {
&STUB_ID
}
fn serialize_request(
&self,
_input: &dyn SerializableStruct,
_input_schema: &Schema<'_>,
_endpoint: &str,
_cfg: &ConfigBag,
) -> Result<Request, SerdeError> {
unimplemented!()
}
fn deserialize_response<'a>(
&self,
_response: &'a Response,
_output_schema: &Schema<'_>,
_cfg: &ConfigBag,
) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
unimplemented!()
}
fn update_endpoint(
&self,
request: &mut Request,
endpoint: &Endpoint,
cfg: &ConfigBag,
) -> Result<(), SerdeError> {
apply_http_endpoint(request, endpoint, cfg)
}
}
fn request_with_uri(uri: &str) -> Request {
let mut req = Request::new(SdkBody::empty());
req.set_uri(uri).unwrap();
req
}
#[test]
fn basic_endpoint() {
let proto = StubProtocol;
let mut req = request_with_uri("/original/path");
let endpoint = Endpoint::builder()
.url("https://service.us-east-1.amazonaws.com")
.build();
let cfg = ConfigBag::base();
ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
assert_eq!(
req.uri(),
"https://service.us-east-1.amazonaws.com/original/path"
);
}
#[test]
fn endpoint_with_prefix() {
let proto = StubProtocol;
let mut req = request_with_uri("/path");
let endpoint = Endpoint::builder()
.url("https://service.us-east-1.amazonaws.com")
.build();
let mut cfg = ConfigBag::base();
let mut layer = Layer::new("test");
layer.store_put(
aws_smithy_runtime_api::client::endpoint::EndpointPrefix::new("myprefix.").unwrap(),
);
cfg.push_shared_layer(layer.freeze());
ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
assert_eq!(
req.uri(),
"https://myprefix.service.us-east-1.amazonaws.com/path"
);
}
#[test]
fn endpoint_with_headers() {
let proto = StubProtocol;
let mut req = request_with_uri("/path");
let endpoint = Endpoint::builder()
.url("https://example.com")
.header("x-custom", "value1")
.header("x-custom", "value2")
.build();
let cfg = ConfigBag::base();
ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
assert_eq!(req.uri(), "https://example.com/path");
let values: Vec<&str> = req.headers().get_all("x-custom").collect();
assert_eq!(values, vec!["value1", "value2"]);
}
#[test]
fn endpoint_with_path() {
let proto = StubProtocol;
let mut req = request_with_uri("/operation");
let endpoint = Endpoint::builder().url("https://example.com/base").build();
let cfg = ConfigBag::base();
ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
assert_eq!(req.uri(), "https://example.com/base/operation");
}
#[test]
fn parse_error_metadata_default_returns_empty_builder() {
let proto = StubProtocol;
let response = Response::new(StatusCode::try_from(500).unwrap(), SdkBody::empty());
let cfg = ConfigBag::base();
let builder = ClientProtocolInner::parse_error_metadata(&proto, &response, &cfg).unwrap();
let meta = builder.build();
assert!(meta.code().is_none());
assert!(meta.message().is_none());
}
#[derive(Debug, Default)]
struct RecordingProtocol {
last_schema_id: std::sync::Mutex<Option<String>>,
}
static REC_ID: ShapeId<'static> =
ShapeId::from_parts("test#RecordingProtocol", "test", "RecordingProtocol");
impl ClientProtocolInner for RecordingProtocol {
type Request = Request;
type Response = Response;
fn protocol_id(&self) -> &ShapeId<'static> {
&REC_ID
}
fn serialize_request(
&self,
_input: &dyn SerializableStruct,
_input_schema: &Schema<'_>,
_endpoint: &str,
_cfg: &ConfigBag,
) -> Result<Request, SerdeError> {
unimplemented!()
}
fn deserialize_response<'a>(
&self,
_response: &'a Response,
output_schema: &Schema<'_>,
_cfg: &ConfigBag,
) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
*self
.last_schema_id
.lock()
.expect("RecordingProtocol mutex poisoned") =
Some(output_schema.shape_id().as_str().to_owned());
Err(SerdeError::custom("recording stub"))
}
fn update_endpoint(
&self,
_request: &mut Request,
_endpoint: &Endpoint,
_cfg: &ConfigBag,
) -> Result<(), SerdeError> {
unimplemented!()
}
}
#[test]
fn deserialize_error_response_default_forwards_with_prelude_document_schema() {
let proto = RecordingProtocol::default();
let response = Response::new(StatusCode::try_from(500).unwrap(), SdkBody::empty());
let cfg = ConfigBag::base();
let _ = ClientProtocolInner::deserialize_error_response(&proto, &response, &cfg);
let observed = proto
.last_schema_id
.lock()
.expect("RecordingProtocol mutex poisoned")
.clone()
.expect("schema id was captured");
assert_eq!(observed, crate::prelude::DOCUMENT.shape_id().as_str());
}
}