Refactor middleware to use new `operation::Response` instead of `http::Response<SdkBody>` (#635)

* Refactor Smithy service tower to include a property bag with the response

* Add doc comments and rename functions on operation Request/Response

* Fix codegen

* Update CHANGELOG

* Attach PR number to CHANGELOG

* Fix test compile error

* CR feedback

* Fix AWS runtime tests

* Fix doc comment link

* Fix API gateway customization

* Fix AWS integration tests and update design doc

* Make it possible to run IAM test outside CI and fix it
This commit is contained in:
John DiSanti 2021-08-05 17:44:58 -07:00 committed by GitHub
parent 8aa3a74af9
commit 68984dc408
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 255 additions and 143 deletions

View File

@ -3,6 +3,9 @@ vNext (Month Day Year)
**Breaking changes**
- (#635) The `config()`, `config_mut()`, `request()`, and `request_mut()` methods on `operation::Request` have been renamed to `properties()`, `properties_mut()`, `http()`, and `http_mut()` respectively.
- (#635) The `Response` type on Tower middleware has been changed from `http::Response<SdkBody>` to `operation::Response`. The HTTP response is still available from the `operation::Response` using its `http()` and `http_mut()` methods.
- (#635) The `ParseHttpResponse` trait's `parse_unloaded()` method now takes an `operation::Response` rather than an `http::Response<SdkBody>`.
- (#626) `ParseHttpResponse` no longer has a generic argument for the body type, but instead, always uses `SdkBody`. This may cause compilation failures for you if you are using Smithy generated types to parse JSON or XML without using a client to request data from a service. The fix should be as simple as removing `<SdkBody>` in the example below:
Before:

View File

@ -73,8 +73,8 @@ impl AsyncMapRequest for CredentialsStage {
fn apply(&self, mut request: Request) -> BoxFuture<Result<Request, Self::Error>> {
Box::pin(async move {
let provider = {
let config = request.config();
let credential_provider = config
let properties = request.properties();
let credential_provider = properties
.get::<CredentialsProvider>()
.ok_or(CredentialsStageError::MissingCredentialsProvider)?;
// we need to enable releasing the config lock so that we don't hold the config
@ -83,7 +83,7 @@ impl AsyncMapRequest for CredentialsStage {
};
let cred_future = { provider.provide_credentials() };
let credentials = cred_future.await?;
request.config_mut().insert(credentials);
request.properties_mut().insert(credentials);
Ok(request)
})
}
@ -112,7 +112,7 @@ mod tests {
async fn async_map_request_apply_populates_credentials() {
let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body")));
set_provider(
&mut req.config_mut(),
&mut req.properties_mut(),
Arc::new(Credentials::from_keys("test", "test", None)),
);
let req = CredentialsStage::new()
@ -120,7 +120,7 @@ mod tests {
.await
.expect("credential provider is in the bag; should succeed");
assert!(
req.config().get::<Credentials>().is_some(),
req.properties().get::<Credentials>().is_some(),
"it should set credentials on the request config"
);
}

View File

@ -145,12 +145,12 @@ impl ResolveAwsEndpoint for Endpoint {
}
type AwsEndpointResolver = Arc<dyn ResolveAwsEndpoint>;
pub fn get_endpoint_resolver(config: &PropertyBag) -> Option<&AwsEndpointResolver> {
config.get()
pub fn get_endpoint_resolver(properties: &PropertyBag) -> Option<&AwsEndpointResolver> {
properties.get()
}
pub fn set_endpoint_resolver(config: &mut PropertyBag, provider: AwsEndpointResolver) {
config.insert(provider);
pub fn set_endpoint_resolver(properties: &mut PropertyBag, provider: AwsEndpointResolver) {
properties.insert(provider);
}
/// Middleware Stage to Add an Endpoint to a Request
@ -182,10 +182,10 @@ impl MapRequest for AwsEndpointStage {
type Error = AwsEndpointStageError;
fn apply(&self, request: Request) -> Result<Request, Self::Error> {
request.augment(|mut http_req, config| {
request.augment(|mut http_req, props| {
let provider =
get_endpoint_resolver(config).ok_or(AwsEndpointStageError::NoEndpointResolver)?;
let region = config
get_endpoint_resolver(props).ok_or(AwsEndpointStageError::NoEndpointResolver)?;
let region = props
.get::<Region>()
.ok_or(AwsEndpointStageError::NoRegion)?;
let endpoint = provider
@ -195,13 +195,13 @@ impl MapRequest for AwsEndpointStage {
.credential_scope
.region
.unwrap_or_else(|| region.clone().into());
config.insert::<SigningRegion>(signing_region);
props.insert::<SigningRegion>(signing_region);
if let Some(signing_service) = endpoint.credential_scope.service {
config.insert::<SigningService>(signing_service);
props.insert::<SigningService>(signing_service);
}
endpoint
.endpoint
.set_endpoint(http_req.uri_mut(), config.get::<EndpointPrefix>());
.set_endpoint(http_req.uri_mut(), props.get::<EndpointPrefix>());
// host is only None if authority is not. `set_endpoint` guarantees that authority is not None
let host = http_req
.uri()
@ -243,18 +243,18 @@ mod test {
let region = Region::new("us-east-1");
let mut req = operation::Request::new(req);
{
let mut conf = req.config_mut();
conf.insert(region.clone());
conf.insert(SigningService::from_static("kinesis"));
set_endpoint_resolver(&mut conf, provider);
let mut props = req.properties_mut();
props.insert(region.clone());
props.insert(SigningService::from_static("kinesis"));
set_endpoint_resolver(&mut props, provider);
};
let req = AwsEndpointStage.apply(req).expect("should succeed");
assert_eq!(
req.config().get(),
req.properties().get(),
Some(&SigningRegion::from(region.clone()))
);
assert_eq!(
req.config().get(),
req.properties().get(),
Some(&SigningService::from_static("kinesis"))
);
@ -284,18 +284,18 @@ mod test {
let region = Region::new("us-east-1");
let mut req = operation::Request::new(req);
{
let mut conf = req.config_mut();
conf.insert(region.clone());
conf.insert(SigningService::from_static("kinesis"));
set_endpoint_resolver(&mut conf, provider);
let mut props = req.properties_mut();
props.insert(region.clone());
props.insert(SigningService::from_static("kinesis"));
set_endpoint_resolver(&mut props, provider);
};
let req = AwsEndpointStage.apply(req).expect("should succeed");
assert_eq!(
req.config().get(),
req.properties().get(),
Some(&SigningRegion::from(Region::new("us-east-override")))
);
assert_eq!(
req.config().get(),
req.properties().get(),
Some(&SigningService::from_static("qldb-override"))
);
}

View File

@ -64,6 +64,7 @@ where
Err(_) => return RetryKind::NotRetryable,
};
if let Some(retry_after_delay) = response
.http()
.headers()
.get("x-amz-retry-after")
.and_then(|header| header.to_str().ok())
@ -82,7 +83,7 @@ where
return RetryKind::Error(ErrorKind::TransientError);
}
};
if TRANSIENT_ERROR_STATUS_CODES.contains(&response.status().as_u16()) {
if TRANSIENT_ERROR_STATUS_CODES.contains(&response.http().status().as_u16()) {
return RetryKind::Error(ErrorKind::TransientError);
};
// TODO: is IDPCommunicationError modeled yet?
@ -94,6 +95,7 @@ where
mod test {
use crate::AwsErrorRetryPolicy;
use smithy_http::body::SdkBody;
use smithy_http::operation;
use smithy_http::result::{SdkError, SdkSuccess};
use smithy_http::retry::ClassifyResponse;
use smithy_types::retry::{ErrorKind, ProvideErrorKind, RetryKind};
@ -131,7 +133,7 @@ mod test {
) -> Result<SdkSuccess<()>, SdkError<E>> {
Err(SdkError::ServiceError {
err,
raw: raw.map(|b| SdkBody::from(b)),
raw: operation::Response::new(raw.map(|b| SdkBody::from(b))),
})
}

View File

@ -16,7 +16,7 @@ use thiserror::Error;
/// AWS User Agent
///
/// Ths struct should be inserted into the [`PropertyBag`](smithy_http::operation::Request::config)
/// Ths struct should be inserted into the [`PropertyBag`](smithy_http::operation::Request::properties)
/// during operation construction. [`UserAgentStage`](UserAgentStage) reads `AwsUserAgent`
/// from the property bag and sets the `User-Agent` and `x-amz-user-agent` headers.
pub struct AwsUserAgent {
@ -298,7 +298,7 @@ mod test {
.apply(req)
.expect_err("adding UA should fail without a UA set");
let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body")));
req.config_mut()
req.properties_mut()
.insert(AwsUserAgent::new_from_environment(ApiMetadata {
service_id: "dynamodb".into(),
version: "0.123",

View File

@ -14,7 +14,7 @@ use aws_types::region::Region;
use aws_types::SigningService;
use bytes::Bytes;
use http::header::{AUTHORIZATION, HOST, USER_AGENT};
use http::{Response, Uri};
use http::{self, Uri};
use smithy_client::test_connection::TestConnection;
use smithy_http::body::SdkBody;
use smithy_http::operation;
@ -57,15 +57,15 @@ impl ProvideErrorKind for OperationError {
impl ParseHttpResponse for TestOperationParser {
type Output = Result<String, OperationError>;
fn parse_unloaded(&self, response: &mut Response<SdkBody>) -> Option<Self::Output> {
if response.status().is_success() {
fn parse_unloaded(&self, response: &mut operation::Response) -> Option<Self::Output> {
if response.http().status().is_success() {
Some(Ok("Hello!".to_string()))
} else {
Some(Err(OperationError))
}
}
fn parse_loaded(&self, _response: &Response<Bytes>) -> Self::Output {
fn parse_loaded(&self, _response: &http::Response<Bytes>) -> Self::Output {
Ok("Hello!".to_string())
}
}

View File

@ -151,13 +151,13 @@ mod test {
);
let mut config = OperationSigningConfig::default_config();
config.signing_options.content_sha256_header = true;
req.config_mut().insert(config);
req.properties_mut().insert(config);
errs.push(
signer
.apply(req.try_clone().expect("can clone"))
.expect_err("no cred provider"),
);
req.config_mut()
req.properties_mut()
.insert(Credentials::from_keys("AKIAfoo", "bar", None));
let req = signer.apply(req).expect("signing succeeded");
// make sure we got the correct error types in any order

View File

@ -115,7 +115,7 @@ class EndpointResolverFeature(private val runtimeConfig: RuntimeConfig, private
is OperationSection.MutateRequest -> writable {
rust(
"""
#T::set_endpoint_resolver(&mut ${section.request}.config_mut(), ${section.config}.endpoint_resolver.clone());
#T::set_endpoint_resolver(&mut ${section.request}.properties_mut(), ${section.config}.endpoint_resolver.clone());
""",
runtimeConfig.awsEndpointDependency().asType()
)

View File

@ -90,7 +90,7 @@ class CredentialsProviderFeature(private val runtimeConfig: RuntimeConfig) : Ope
is OperationSection.MutateRequest -> writable {
rust(
"""
#T(&mut ${section.request}.config_mut(), ${section.config}.credentials_provider.clone());
#T(&mut ${section.request}.properties_mut(), ${section.config}.credentials_provider.clone());
""",
setProvider(runtimeConfig)
)

View File

@ -110,7 +110,7 @@ class RegionConfigPlugin : OperationCustomization() {
rust(
"""
if let Some(region) = &${section.config}.region {
${section.request}.config_mut().insert(region.clone());
${section.request}.properties_mut().insert(region.clone());
}
"""
)

View File

@ -119,14 +119,14 @@ class SigV4SigningFeature(
if (operation.hasTrait<UnsignedPayloadTrait>()) {
rust("signing_config.signing_options.content_sha256_header = true;")
rustTemplate(
"${section.request}.config_mut().insert(#{sig_auth}::signer::SignableBody::UnsignedPayload);",
"${section.request}.properties_mut().insert(#{sig_auth}::signer::SignableBody::UnsignedPayload);",
*codegenScope
)
}
rustTemplate(
"""
${section.request}.config_mut().insert(signing_config);
${section.request}.config_mut().insert(#{aws_types}::SigningService::from_static(${section.config}.signing_service()));
${section.request}.properties_mut().insert(signing_config);
${section.request}.properties_mut().insert(#{aws_types}::SigningService::from_static(${section.config}.signing_service()));
""",
*codegenScope
)

View File

@ -67,7 +67,7 @@ class UserAgentFeature(private val runtimeConfig: RuntimeConfig) : OperationCust
is OperationSection.MutateRequest -> writable {
rust(
"""
${section.request}.config_mut().insert(#T::AwsUserAgent::new_from_environment(crate::API_METADATA.clone()));
${section.request}.properties_mut().insert(#T::AwsUserAgent::new_from_environment(crate::API_METADATA.clone()));
""",
runtimeConfig.userAgentModule()
)

View File

@ -40,7 +40,7 @@ class ApiGatewayAddAcceptHeader : OperationCustomization() {
is OperationSection.MutateRequest -> writable {
rust(
"""${section.request}
.request_mut()
.http_mut()
.headers_mut()
.insert("Accept", #T::HeaderValue::from_static("application/json"));""",
RuntimeType.http

View File

@ -0,0 +1,22 @@
# This Cargo.toml is unused in generated code. It exists solely to enable these tests to compile in-situ
[package]
name = "iam-tests"
version = "0.1.0"
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
aws-sdk-iam = { path = "../../build/aws-sdk/iam" }
aws-endpoint = { path = "../../build/aws-sdk/aws-endpoint" }
smithy-client = { path = "../../build/aws-sdk/smithy-client", features = ["test-util"] }
smithy-http = { path = "../../build/aws-sdk/smithy-http" }
tracing-subscriber = "0.2.18"
[dev-dependencies]
tokio = { version = "1", features = ["full"]}
http = "0.2.3"
bytes = "1"
aws-hyper = { path = "../../build/aws-sdk/aws-hyper"}
aws-http = { path = "../../build/aws-sdk/aws-http"}

View File

@ -0,0 +1,4 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/

View File

@ -15,8 +15,8 @@ fn correct_endpoint_resolver() {
.unwrap()
.make_operation(&conf)
.expect("valid operation");
let conf = operation.config();
let resolver = get_endpoint_resolver(&conf).expect("operation should have endpoint resolver");
let props = operation.properties();
let resolver = get_endpoint_resolver(&props).expect("operation should have endpoint resolver");
// test regular endpoint
{
let ep = resolver

View File

@ -9,10 +9,11 @@ edition = "2018"
[dependencies]
aws-sdk-kms = { path = "../../build/aws-sdk/kms" }
smithy-client = { path = "../../build/aws-sdk/smithy-client", features = ["test-util"] }
smithy-http = { path = "../../build/aws-sdk/smithy-http" }
smithy-types = { path = "../../build/aws-sdk/smithy-types" }
http = "0.2.3"
aws-hyper = { path = "../../build/aws-sdk/aws-hyper", features = ["test-util"] }
aws-hyper = { path = "../../build/aws-sdk/aws-hyper" }
aws-auth = { path = "../../build/aws-sdk/aws-auth" }
aws-http = { path = "../../build/aws-sdk/aws-http" }
tokio = { version = "1", features = ["full"]}

View File

@ -89,9 +89,9 @@ async fn generate_random() {
.unwrap()
.make_operation(&conf)
.expect("valid operation");
op.config_mut()
op.properties_mut()
.insert(UNIX_EPOCH + Duration::from_secs(1614952162));
op.config_mut().insert(AwsUserAgent::for_tests());
op.properties_mut().insert(AwsUserAgent::for_tests());
let resp = client.call(op).await.expect("request should succeed");
// primitive checksum
assert_eq!(
@ -181,9 +181,9 @@ async fn generate_random_keystore_not_found() {
.make_operation(&conf)
.expect("valid operation");
op.config_mut()
op.properties_mut()
.insert(UNIX_EPOCH + Duration::from_secs(1614955644));
op.config_mut().insert(AwsUserAgent::for_tests());
op.properties_mut().insert(AwsUserAgent::for_tests());
let client = Client::new(conn.clone());
let err = client.call(op).await.expect_err("key store doesn't exist");
let inner = match err {

View File

@ -11,7 +11,7 @@ use kms::operation::{CreateAlias, GenerateRandom};
use kms::output::GenerateRandomOutput;
use kms::Blob;
use smithy_http::body::SdkBody;
use smithy_http::operation::Parts;
use smithy_http::operation::{self, Parts};
use smithy_http::response::ParseStrictResponse;
use smithy_http::result::SdkError;
use smithy_http::retry::ClassifyResponse;
@ -88,7 +88,7 @@ fn errors_are_retryable() {
.parse(&http_response)
.map_err(|e| SdkError::ServiceError {
err: e,
raw: http_response.map(SdkBody::from),
raw: operation::Response::new(http_response.map(SdkBody::from)),
});
let retry_kind = op.retry_policy.classify(err.as_ref());
assert_eq!(retry_kind, RetryKind::Error(ErrorKind::ThrottlingError));
@ -106,7 +106,7 @@ fn unmodeled_errors_are_retryable() {
.parse(&http_response)
.map_err(|e| SdkError::ServiceError {
err: e,
raw: http_response.map(SdkBody::from),
raw: operation::Response::new(http_response.map(SdkBody::from)),
});
let retry_kind = op.retry_policy.classify(err.as_ref());
assert_eq!(retry_kind, RetryKind::Error(ErrorKind::ThrottlingError));

View File

@ -9,10 +9,11 @@ edition = "2018"
[dependencies]
aws-sdk-qldbsession = { path = "../../build/aws-sdk/qldbsession" }
smithy-client = { path = "../../build/aws-sdk/smithy-client", features = ["test-util"] }
smithy-http = { path = "../../build/aws-sdk/smithy-http" }
smithy-types = { path = "../../build/aws-sdk/smithy-types" }
http = "0.2.3"
aws-hyper = { path = "../../build/aws-sdk/aws-hyper", features = ["test-util"] }
aws-hyper = { path = "../../build/aws-sdk/aws-hyper" }
aws-auth = { path = "../../build/aws-sdk/aws-auth" }
aws-http = { path = "../../build/aws-sdk/aws-http" }
tokio = { version = "1", features = ["full"]}

View File

@ -61,9 +61,9 @@ async fn signv4_use_correct_service_name() {
.make_operation(&conf)
.expect("valid operation");
// Fix the request time and user agent so the headers are stable
op.config_mut()
op.properties_mut()
.insert(UNIX_EPOCH + Duration::from_secs(1614952162));
op.config_mut().insert(AwsUserAgent::for_tests());
op.properties_mut().insert(AwsUserAgent::for_tests());
let _ = client.call(op).await.expect("request should succeed");

View File

@ -9,6 +9,7 @@ edition = "2018"
[dependencies]
aws-sdk-s3 = { path = "../../build/aws-sdk/s3" }
smithy-client = { path = "../../build/aws-sdk/smithy-client", features = ["test-util"] }
smithy-http = { path = "../../build/aws-sdk/smithy-http" }
tracing-subscriber = "0.2.18"

View File

@ -37,9 +37,9 @@ async fn test_signer() -> Result<(), aws_sdk_s3::Error> {
.unwrap()
.make_operation(&conf)
.unwrap();
op.config_mut()
op.properties_mut()
.insert(UNIX_EPOCH + Duration::from_secs(1624036048));
op.config_mut().insert(AwsUserAgent::for_tests());
op.properties_mut().insert(AwsUserAgent::for_tests());
client.call(op).await.expect_err("empty response");
for req in conn.requests().iter() {

View File

@ -35,7 +35,7 @@ class EndpointPrefixGenerator(private val protocolConfig: ProtocolConfig, privat
endpointTraitBindings.render(this, "self")
}
rustBlock("match endpoint_prefix") {
rust("Ok(prefix) => { request.config_mut().insert(prefix); },")
rust("Ok(prefix) => { request.properties_mut().insert(prefix); },")
rust("Err(err) => return Err(${buildError.serializationError(this, "err")})")
}
}

View File

@ -258,7 +258,7 @@ class HttpProtocolTestGenerator(
writeInline("let expected_output =")
instantiator.render(this, expectedShape, testCase.params)
write(";")
write("let mut http_response = #T::new()", RuntimeType.HttpResponseBuilder)
write("let http_response = #T::new()", RuntimeType.HttpResponseBuilder)
testCase.headers.forEach { (key, value) ->
writeWithNoFormatting(".header(${key.dq()}, ${value.dq()})")
}
@ -270,12 +270,17 @@ class HttpProtocolTestGenerator(
""",
RuntimeType.sdkBody(runtimeConfig = protocolConfig.runtimeConfig)
)
write(
"let mut op_response = #T::new(http_response);",
RuntimeType.operationModule(protocolConfig.runtimeConfig).member("Response")
)
rustTemplate(
"""
use #{parse_http_response};
let parser = #{op}::new();
let parsed = parser.parse_unloaded(&mut http_response);
let parsed = parser.parse_unloaded(&mut op_response);
let parsed = parsed.unwrap_or_else(|| {
let (http_response, _) = op_response.into_parts();
let http_response = http_response.map(|body|#{bytes}::copy_from_slice(body.bytes().unwrap()));
<#{op} as #{parse_http_response}>::parse_loaded(&parser, &http_response)
});

View File

@ -80,7 +80,8 @@ class HttpBoundProtocolGenerator(
private val codegenScope = arrayOf(
"ParseStrict" to RuntimeType.parseStrict(runtimeConfig),
"ParseResponse" to RuntimeType.parseResponse(runtimeConfig),
"Response" to RuntimeType.Http("Response"),
"http" to RuntimeType.http,
"operation" to RuntimeType.operationModule(runtimeConfig),
"Bytes" to RuntimeType.Bytes,
"SdkBody" to RuntimeType.sdkBody(runtimeConfig),
"BuildError" to runtimeConfig.operationBuildError()
@ -221,7 +222,7 @@ class HttpBoundProtocolGenerator(
"""
impl #{ParseStrict} for $operationName {
type Output = std::result::Result<#{O}, #{E}>;
fn parse(&self, response: &#{Response}<#{Bytes}>) -> Self::Output {
fn parse(&self, response: &#{http}::Response<#{Bytes}>) -> Self::Output {
if !response.status().is_success() && response.status().as_u16() != $successCode {
#{parse_error}(response)
} else {
@ -247,14 +248,14 @@ class HttpBoundProtocolGenerator(
"""
impl #{ParseResponse} for $operationName {
type Output = std::result::Result<#{O}, #{E}>;
fn parse_unloaded(&self, response: &mut http::Response<#{SdkBody}>) -> Option<Self::Output> {
fn parse_unloaded(&self, response: &mut #{operation}::Response) -> Option<Self::Output> {
// This is an error, defer to the non-streaming parser
if !response.status().is_success() && response.status().as_u16() != $successCode {
if !response.http().status().is_success() && response.http().status().as_u16() != $successCode {
return None;
}
Some(#{parse_streaming_response}(response))
}
fn parse_loaded(&self, response: &http::Response<#{Bytes}>) -> Self::Output {
fn parse_loaded(&self, response: &#{http}::Response<#{Bytes}>) -> Self::Output {
// if streaming, we only hit this case if its an error
#{parse_error}(response)
}
@ -276,7 +277,7 @@ class HttpBoundProtocolGenerator(
return RuntimeType.forInlineFun(fnName, "operation_deser") {
Attribute.Custom("allow(clippy::unnecessary_wraps)").render(it)
it.rustBlockTemplate(
"pub fn $fnName(response: &#{Response}<#{Bytes}>) -> std::result::Result<#{O}, #{E}>",
"pub fn $fnName(response: &#{http}::Response<#{Bytes}>) -> std::result::Result<#{O}, #{E}>",
*codegenScope,
"O" to outputSymbol,
"E" to errorSymbol
@ -350,11 +351,12 @@ class HttpBoundProtocolGenerator(
return RuntimeType.forInlineFun(fnName, "operation_deser") {
Attribute.Custom("allow(clippy::unnecessary_wraps)").render(it)
it.rustBlockTemplate(
"pub fn $fnName(response: &mut #{Response}<#{SdkBody}>) -> std::result::Result<#{O}, #{E}>",
"pub fn $fnName(op_response: &mut #{operation}::Response) -> std::result::Result<#{O}, #{E}>",
*codegenScope,
"O" to outputSymbol,
"E" to errorSymbol
) {
write("let response = op_response.http_mut();")
withBlock("Ok({", "})") {
renderShapeParser(
operationShape,
@ -375,7 +377,7 @@ class HttpBoundProtocolGenerator(
return RuntimeType.forInlineFun(fnName, "operation_deser") {
Attribute.Custom("allow(clippy::unnecessary_wraps)").render(it)
it.rustBlockTemplate(
"pub fn $fnName(response: &#{Response}<#{Bytes}>) -> std::result::Result<#{O}, #{E}>",
"pub fn $fnName(response: &#{http}::Response<#{Bytes}>) -> std::result::Result<#{O}, #{E}>",
*codegenScope,
"O" to outputSymbol,
"E" to errorSymbol

View File

@ -151,8 +151,8 @@ internal class EndpointTraitBindingsTest {
.greeting("hello")
.build().expect("valid operation")
.make_operation(&conf).expect("hello is a valid prefix");
let op_conf = op.config();
let prefix = op_conf.get::<smithy_http::endpoint::EndpointPrefix>()
let properties = op.properties();
let prefix = properties.get::<smithy_http::endpoint::EndpointPrefix>()
.expect("prefix should be in config")
.as_str();
assert_eq!(prefix, "test123.hello.");

View File

@ -24,8 +24,8 @@ pub struct Operation<H, R> {
}
pub struct Request {
base: http::Request<SdkBody>,
configuration: PropertyBag,
inner: http::Request<SdkBody>,
properties: PropertyBag,
}
```
@ -46,9 +46,9 @@ pub fn build(self, config: &dynamodb::config::Config) -> Operation<BatchExecuteS
let req = op.build_http_request().map(SdkBody::from);
let mut req = operation::Request::new(req);
let mut conf = req.config_mut();
conf.insert_signing_config(config.signing_service());
conf.insert_endpoint_resolver(config.endpoint_resolver.clone());
let mut props = req.properties_mut();
props.insert_signing_config(config.signing_service());
props.insert_endpoint_resolver(config.endpoint_resolver.clone());
Operation::new(req)
}
```

View File

@ -62,7 +62,7 @@ where
pub trait SmithyMiddlewareService:
Service<
smithy_http::operation::Request,
Response = http::Response<SdkBody>,
Response = smithy_http::operation::Response,
Error = smithy_http_tower::SendOperationError,
Future = <Self as SmithyMiddlewareService>::Future,
>
@ -77,7 +77,7 @@ impl<T> SmithyMiddlewareService for T
where
T: Service<
smithy_http::operation::Request,
Response = http::Response<SdkBody>,
Response = smithy_http::operation::Response,
Error = smithy_http_tower::SendOperationError,
>,
T::Future: Send + 'static,

View File

@ -178,7 +178,7 @@ pub struct DynMiddleware<C>(
BoxCloneLayer<
smithy_http_tower::dispatch::DispatchService<C>,
smithy_http::operation::Request,
http::Response<SdkBody>,
smithy_http::operation::Response,
smithy_http_tower::SendOperationError,
>,
);
@ -199,7 +199,7 @@ impl<C> DynMiddleware<C> {
impl<C> Layer<smithy_http_tower::dispatch::DispatchService<C>> for DynMiddleware<C> {
type Service = BoxCloneService<
smithy_http::operation::Request,
http::Response<SdkBody>,
smithy_http::operation::Response,
smithy_http_tower::SendOperationError,
>;

View File

@ -1,7 +1,12 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
//! This module provides types useful for static tests.
#![allow(missing_docs, missing_debug_implementations)]
use crate::*;
use crate::{Builder, Error, Operation, ParseHttpResponse, ProvideErrorKind};
use smithy_http::operation;
#[derive(Debug)]
#[non_exhaustive]
@ -27,7 +32,7 @@ pub struct TestOperation;
impl ParseHttpResponse for TestOperation {
type Output = Result<(), TestOperationError>;
fn parse_unloaded(&self, _: &mut http::Response<SdkBody>) -> Option<Self::Output> {
fn parse_unloaded(&self, _: &mut operation::Response) -> Option<Self::Output> {
unreachable!("only used for static tests")
}
@ -51,7 +56,7 @@ fn sanity_retry() {
// Statically check that a hyper client can actually be used to build a Client.
#[allow(dead_code)]
#[cfg(all(test, feature = "hyper"))]
fn sanity_hyper<C>(hc: hyper::Client<C, SdkBody>)
fn sanity_hyper<C>(hc: hyper::Client<C, smithy_http::body::SdkBody>)
where
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{

View File

@ -25,11 +25,11 @@ type BoxedResultFuture<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send>
impl<S> Service<operation::Request> for DispatchService<S>
where
S: Service<http::Request<SdkBody>> + Clone + Send + 'static,
S: Service<http::Request<SdkBody>, Response = http::Response<SdkBody>> + Clone + Send + 'static,
S::Error: Into<BoxError>,
S::Future: Send + 'static,
{
type Response = S::Response;
type Response = operation::Response;
type Error = SendOperationError;
type Future = BoxedResultFuture<Self::Response, Self::Error>;
@ -40,13 +40,14 @@ where
}
fn call(&mut self, req: operation::Request) -> Self::Future {
let (req, _property_bag) = req.into_parts();
let (req, property_bag) = req.into_parts();
let mut inner = self.inner.clone();
let future = async move {
trace!(request = ?req);
inner
.call(req)
.await
.map(|resp| operation::Response::from_parts(resp, property_bag))
.map_err(|e| SendOperationError::RequestDispatchError(e.into()))
};
Box::pin(future)

View File

@ -4,7 +4,6 @@
*/
use crate::SendOperationError;
use smithy_http::body::SdkBody;
use smithy_http::middleware::load_response;
use smithy_http::operation;
use smithy_http::operation::Operation;
@ -46,7 +45,7 @@ impl<O, R> ParseResponseLayer<O, R> {
impl<S, O, R> Layer<S> for ParseResponseLayer<O, R>
where
S: Service<operation::Request>,
S: Service<operation::Request, Response = operation::Response>,
{
type Service = ParseResponseService<S, O, R>;
@ -67,11 +66,10 @@ type BoxedResultFuture<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send>
/// `O`: The type of the response parser whose output type is `Result<T, E>`
/// `T`: The happy path return of the response parser
/// `E`: The error path return of the response parser
/// `B`: The HTTP Body type returned by the inner service
/// `R`: The type of the retry policy
impl<S, O, T, E, R> tower::Service<operation::Operation<O, R>> for ParseResponseService<S, O, R>
where
S: Service<operation::Request, Response = http::Response<SdkBody>, Error = SendOperationError>,
S: Service<operation::Request, Response = operation::Response, Error = SendOperationError>,
S::Future: Send + 'static,
O: ParseHttpResponse<Output = Result<T, E>> + Send + Sync + 'static,
E: Error,
@ -107,7 +105,7 @@ where
Err(e) => Err(e.into()),
Ok(resp) => {
// load_response contains reading the body as far as is required & parsing the response
let response_span = debug_span!("load_response",);
let response_span = debug_span!("load_response");
load_response(resp, &handler)
.instrument(response_span)
.await

View File

@ -14,7 +14,6 @@ use crate::pin_mut;
use crate::response::ParseHttpResponse;
use crate::result::{SdkError, SdkSuccess};
use bytes::{Buf, Bytes};
use http::Response;
use http_body::Body;
use std::error::Error;
use std::future::Future;
@ -82,11 +81,10 @@ pub trait MapRequest {
///
/// Success and failure will be split and mapped into `SdkSuccess` and `SdkError`.
/// Generic Parameters:
/// - `B`: The Response Body
/// - `O`: The Http response handler that returns `Result<T, E>`
/// - `T`/`E`: `Result<T, E>` returned by `handler`.
pub async fn load_response<T, E, O>(
mut response: http::Response<SdkBody>,
mut response: operation::Response,
handler: &O,
) -> Result<SdkSuccess<T>, SdkError<E>>
where
@ -97,21 +95,28 @@ where
return sdk_result(parsed_response, response);
}
let (parts, body) = response.into_parts();
let (http_response, properties) = response.into_parts();
let (parts, body) = http_response.into_parts();
let body = match read_body(body).await {
Ok(body) => body,
Err(err) => {
return Err(SdkError::ResponseError {
raw: Response::from_parts(parts, SdkBody::taken()),
raw: operation::Response::from_parts(
http::Response::from_parts(parts, SdkBody::taken()),
properties,
),
err,
});
}
};
let response = Response::from_parts(parts, Bytes::from(body));
trace!(response = ?response);
let parsed = handler.parse_loaded(&response);
sdk_result(parsed, response.map(SdkBody::from))
let http_response = http::Response::from_parts(parts, Bytes::from(body));
trace!(http_response = ?http_response);
let parsed = handler.parse_loaded(&http_response);
sdk_result(
parsed,
operation::Response::from_parts(http_response.map(SdkBody::from), properties),
)
}
async fn read_body<B: http_body::Body>(body: B) -> Result<Vec<u8>, B::Error> {
@ -127,10 +132,10 @@ async fn read_body<B: http_body::Body>(body: B) -> Result<Vec<u8>, B::Error> {
Ok(output)
}
/// Convert a `Result<T, E>` into an `SdkResult` that includes the raw HTTP response
/// Convert a `Result<T, E>` into an `SdkResult` that includes the operation response
fn sdk_result<T, E>(
parsed: Result<T, E>,
raw: http::Response<SdkBody>,
raw: operation::Response,
) -> Result<SdkSuccess<T>, SdkError<E>> {
match parsed {
Ok(parsed) => Ok(SdkSuccess { raw, parsed }),

View File

@ -81,12 +81,12 @@ impl<H, R> Operation<H, R> {
Self { request, parts }
}
pub fn config_mut(&mut self) -> impl DerefMut<Target = PropertyBag> + '_ {
self.request.config_mut()
pub fn properties_mut(&mut self) -> impl DerefMut<Target = PropertyBag> + '_ {
self.request.properties_mut()
}
pub fn config(&self) -> impl Deref<Target = PropertyBag> + '_ {
self.request.config()
pub fn properties(&self) -> impl Deref<Target = PropertyBag> + '_ {
self.request.properties()
}
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
@ -135,6 +135,9 @@ impl<H> Operation<H, ()> {
}
}
/// Operation request type that associates a property bag with an underlying HTTP request.
/// This type represents the request in the Tower `Service` in middleware so that middleware
/// can share information with each other via the properties.
#[derive(Debug)]
pub struct Request {
/// The underlying HTTP Request
@ -144,47 +147,56 @@ pub struct Request {
///
/// Middleware can read and write from the property bag and use its
/// contents to augment the request (see [`Request::augment`](Request::augment))
configuration: Arc<Mutex<PropertyBag>>,
properties: Arc<Mutex<PropertyBag>>,
}
impl Request {
pub fn new(base: http::Request<SdkBody>) -> Self {
/// Creates a new operation `Request` with the given `inner` HTTP request.
pub fn new(inner: http::Request<SdkBody>) -> Self {
Request {
inner: base,
configuration: Arc::new(Mutex::new(PropertyBag::new())),
inner,
properties: Arc::new(Mutex::new(PropertyBag::new())),
}
}
/// Allows modification of the HTTP request and associated properties with a fallible closure.
pub fn augment<T>(
self,
f: impl FnOnce(http::Request<SdkBody>, &mut PropertyBag) -> Result<http::Request<SdkBody>, T>,
) -> Result<Request, T> {
let inner = {
let configuration: &mut PropertyBag = &mut self.configuration.lock().unwrap();
f(self.inner, configuration)?
let properties: &mut PropertyBag = &mut self.properties.lock().unwrap();
f(self.inner, properties)?
};
Ok(Request {
inner,
configuration: self.configuration,
properties: self.properties,
})
}
pub fn config_mut(&mut self) -> MutexGuard<'_, PropertyBag> {
self.configuration.lock().unwrap()
/// Gives mutable access to the properties.
pub fn properties_mut(&mut self) -> MutexGuard<'_, PropertyBag> {
self.properties.lock().unwrap()
}
pub fn config(&self) -> MutexGuard<'_, PropertyBag> {
self.configuration.lock().unwrap()
/// Gives readonly access to the properties.
pub fn properties(&self) -> MutexGuard<'_, PropertyBag> {
self.properties.lock().unwrap()
}
pub fn request_mut(&mut self) -> &mut http::Request<SdkBody> {
/// Gives mutable access to the underlying HTTP request.
pub fn http_mut(&mut self) -> &mut http::Request<SdkBody> {
&mut self.inner
}
pub fn request(&self) -> &http::Request<SdkBody> {
/// Gives readonly access to the underlying HTTP request.
pub fn http(&self) -> &http::Request<SdkBody> {
&self.inner
}
/// Attempts to clone the operation `Request`. This can fail if the
/// request body can't be cloned, such as if it is being streamed and the
/// stream can't be recreated.
pub fn try_clone(&self) -> Option<Request> {
let cloned_body = self.inner.body().try_clone()?;
let mut cloned_request = http::Request::builder()
@ -199,12 +211,65 @@ impl Request {
.expect("a clone of a valid request should be a valid request");
Some(Request {
inner,
configuration: self.configuration.clone(),
properties: self.properties.clone(),
})
}
/// Consumes the operation `Request` and returns the underlying HTTP request and properties.
pub fn into_parts(self) -> (http::Request<SdkBody>, Arc<Mutex<PropertyBag>>) {
(self.inner, self.configuration)
(self.inner, self.properties)
}
}
/// Operation response type that associates a property bag with an underlying HTTP response.
/// This type represents the response in the Tower `Service` in middleware so that middleware
/// can share information with each other via the properties.
#[derive(Debug)]
pub struct Response {
/// The underlying HTTP Response
inner: http::Response<SdkBody>,
/// Property bag of configuration options
properties: Arc<Mutex<PropertyBag>>,
}
impl Response {
/// Creates a new operation `Response` with the given `inner` HTTP response.
pub fn new(inner: http::Response<SdkBody>) -> Self {
Response {
inner,
properties: Arc::new(Mutex::new(PropertyBag::new())),
}
}
/// Gives mutable access to the properties.
pub fn properties_mut(&mut self) -> MutexGuard<'_, PropertyBag> {
self.properties.lock().unwrap()
}
/// Gives readonly access to the properties.
pub fn properties(&self) -> MutexGuard<'_, PropertyBag> {
self.properties.lock().unwrap()
}
/// Gives mutable access to the underlying HTTP response.
pub fn http_mut(&mut self) -> &mut http::Response<SdkBody> {
&mut self.inner
}
/// Gives readonly access to the underlying HTTP response.
pub fn http(&self) -> &http::Response<SdkBody> {
&self.inner
}
/// Consumes the operation `Request` and returns the underlying HTTP response and properties.
pub fn into_parts(self) -> (http::Response<SdkBody>, Arc<Mutex<PropertyBag>>) {
(self.inner, self.properties)
}
/// Creates a new operation `Response` from an HTTP response and property bag.
pub fn from_parts(inner: http::Response<SdkBody>, properties: Arc<Mutex<PropertyBag>>) -> Self {
Response { inner, properties }
}
}
@ -226,7 +291,7 @@ mod test {
.body(SdkBody::from("hello world!"))
.expect("valid request"),
);
request.config_mut().insert("hello");
request.properties_mut().insert("hello");
let cloned = request.try_clone().expect("request is cloneable");
let (request, config) = cloned.into_parts();

View File

@ -3,9 +3,8 @@
* SPDX-License-Identifier: Apache-2.0.
*/
use crate::body::SdkBody;
use crate::operation;
use bytes::Bytes;
use http::Response;
/// `ParseHttpResponse` is a generic trait for parsing structured data from HTTP responses.
///
@ -45,7 +44,7 @@ pub trait ParseHttpResponse {
/// the streaming body with an empty body as long as the body implements default.
///
/// We should consider if this is too limiting & if this should take an owned response instead.
fn parse_unloaded(&self, response: &mut http::Response<SdkBody>) -> Option<Self::Output>;
fn parse_unloaded(&self, response: &mut operation::Response) -> Option<Self::Output>;
/// Parse an HTTP request from a fully loaded body. This is for standard request/response style
/// APIs like AwsJson 1.0/1.1 and the error path of most streaming APIs
@ -67,17 +66,17 @@ pub trait ParseHttpResponse {
/// have cleaner implementations. There is a blanket implementation
pub trait ParseStrictResponse {
type Output;
fn parse(&self, response: &Response<Bytes>) -> Self::Output;
fn parse(&self, response: &http::Response<Bytes>) -> Self::Output;
}
impl<T: ParseStrictResponse> ParseHttpResponse for T {
type Output = T::Output;
fn parse_unloaded(&self, _response: &mut Response<SdkBody>) -> Option<Self::Output> {
fn parse_unloaded(&self, _response: &mut operation::Response) -> Option<Self::Output> {
None
}
fn parse_loaded(&self, response: &Response<Bytes>) -> Self::Output {
fn parse_loaded(&self, response: &http::Response<Bytes>) -> Self::Output {
self.parse(response)
}
}
@ -85,9 +84,9 @@ impl<T: ParseStrictResponse> ParseHttpResponse for T {
#[cfg(test)]
mod test {
use crate::body::SdkBody;
use crate::operation;
use crate::response::ParseHttpResponse;
use bytes::Bytes;
use http::Response;
use std::mem;
#[test]
@ -101,13 +100,13 @@ mod test {
impl ParseHttpResponse for S3GetObjectParser {
type Output = S3GetObject;
fn parse_unloaded(&self, response: &mut Response<SdkBody>) -> Option<Self::Output> {
fn parse_unloaded(&self, response: &mut operation::Response) -> Option<Self::Output> {
// For responses that pass on the body, use mem::take to leave behind an empty body
let body = mem::replace(response.body_mut(), SdkBody::taken());
let body = mem::replace(response.http_mut().body_mut(), SdkBody::taken());
Some(S3GetObject { body })
}
fn parse_loaded(&self, _response: &Response<Bytes>) -> Self::Output {
fn parse_loaded(&self, _response: &http::Response<Bytes>) -> Self::Output {
unimplemented!()
}
}

View File

@ -3,20 +3,21 @@
* SPDX-License-Identifier: Apache-2.0.
*/
use crate::body::SdkBody;
use crate::operation;
use std::error::Error;
use std::fmt;
use std::fmt::{Debug, Display, Formatter};
type BoxError = Box<dyn Error + Send + Sync>;
/// Successful Sdk Result
/// Successful SDK Result
#[derive(Debug)]
pub struct SdkSuccess<O> {
pub raw: http::Response<SdkBody>,
pub raw: operation::Response,
pub parsed: O,
}
/// Failing Sdk Result
/// Failed SDK Result
#[derive(Debug)]
pub enum SdkError<E> {
/// The request failed during construction. It was not dispatched over the network.
@ -29,15 +30,12 @@ pub enum SdkError<E> {
/// A response was received but it was not parseable according the the protocol (for example
/// the server hung up while the body was being read)
ResponseError {
raw: http::Response<SdkBody>,
raw: operation::Response,
err: BoxError,
},
/// An error response was received from the service
ServiceError {
err: E,
raw: http::Response<SdkBody>,
},
ServiceError { err: E, raw: operation::Response },
}
impl<E> Display for SdkError<E>