Orchestrator `ResponseDeserializer` codegen and auth (#2494)

* Convert interceptor fns to macro invocations

Co-authored-by: John DiSanti <jdisanti@amazon.com>

* Add unmodeled error to ServiceError and bring in EventLog

Co-authored-by: John DiSanti <jdisanti@amazon.com>

* Simplify and test `EventLog`

* Attempt to integrate the event log with the orchestrator

* fix: error type in integration test
update: retry handling in orchestrator

* update: set the runtime plugins to do nothing instead of panic when called on

* Introduce a construct for type erasure

* Eliminate several generics and add phased error handling

* Code generate the `ResponseDeserializer` trait impls

* CI fixes

* Reorganize the new runtime crates

* Create the `Identity` type

* Reorganize orchestrator traits and accessors

* Set up code generated test environment for the new runtime

* Add initial auth orchestration implementation

* Fix clippy lint

* Incorporate feedback

* Fix external types lint

---------

Co-authored-by: Zelda Hessler <zhessler@amazon.com>
This commit is contained in:
John DiSanti 2023-03-27 16:15:36 -07:00 committed by GitHub
parent b65a645ce1
commit 6b21e46ef1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 2249 additions and 1893 deletions

View File

@ -58,6 +58,12 @@ impl<B> RequestId for http::Response<B> {
}
}
impl RequestId for HeaderMap {
fn request_id(&self) -> Option<&str> {
extract_request_id(self)
}
}
impl<O, E> RequestId for Result<O, E>
where
O: RequestId,

View File

@ -59,6 +59,12 @@ impl<B> RequestIdExt for http::Response<B> {
}
}
impl RequestIdExt for HeaderMap {
fn extended_request_id(&self) -> Option<&str> {
extract_extended_request_id(self)
}
}
impl<O, E> RequestIdExt for Result<O, E>
where
O: RequestIdExt,

View File

@ -3,8 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
extra["displayName"] = "Smithy :: Rust :: Codegen :: Test"
extra["moduleName"] = "software.amazon.smithy.kotlin.codegen.test"
extra["displayName"] = "Smithy :: Rust :: AWS-SDK :: Ad-hoc Test"
extra["moduleName"] = "software.amazon.smithy.rust.awssdk.adhoc.test"
tasks["jar"].enabled = false

View File

@ -83,13 +83,13 @@ abstract class BaseRequestIdDecorator : ClientCodegenDecorator {
when (section) {
is OperationSection.PopulateErrorMetadataExtras -> {
rustTemplate(
"${section.builderName} = #{apply_to_error}(${section.builderName}, ${section.responseName}.headers());",
"${section.builderName} = #{apply_to_error}(${section.builderName}, ${section.responseHeadersName});",
"apply_to_error" to applyToError(codegenContext),
)
}
is OperationSection.MutateOutput -> {
rust(
"output._set_$fieldName(#T::$accessorFunctionName(response).map(str::to_string));",
"output._set_$fieldName(#T::$accessorFunctionName(${section.responseHeadersName}).map(str::to_string));",
accessorTrait(codegenContext),
)
}

View File

@ -22,6 +22,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.generators.operationBuild
import software.amazon.smithy.rust.codegen.core.util.expectMember
import software.amazon.smithy.rust.codegen.core.util.getTrait
import software.amazon.smithy.rust.codegen.core.util.inputShape
import software.amazon.smithy.rust.codegen.core.util.letIf
import software.amazon.smithy.rust.codegen.core.util.orNull
fun RuntimeConfig.awsInlineableBodyWithChecksum() = RuntimeType.forInlineDependency(
@ -41,12 +42,16 @@ class HttpRequestChecksumDecorator : ClientCodegenDecorator {
override val name: String = "HttpRequestChecksum"
override val order: Byte = 0
// TODO(enableNewSmithyRuntime): Implement checksumming via interceptor and delete this decorator
private fun applies(codegenContext: ClientCodegenContext): Boolean =
!codegenContext.settings.codegenConfig.enableNewSmithyRuntime
override fun operationCustomizations(
codegenContext: ClientCodegenContext,
operation: OperationShape,
baseCustomizations: List<OperationCustomization>,
): List<OperationCustomization> {
return baseCustomizations + HttpRequestChecksumCustomization(codegenContext, operation)
): List<OperationCustomization> = baseCustomizations.letIf(applies(codegenContext)) {
it + HttpRequestChecksumCustomization(codegenContext, operation)
}
}

View File

@ -17,6 +17,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationSectio
import software.amazon.smithy.rust.codegen.core.util.expectMember
import software.amazon.smithy.rust.codegen.core.util.getTrait
import software.amazon.smithy.rust.codegen.core.util.inputShape
import software.amazon.smithy.rust.codegen.core.util.letIf
import software.amazon.smithy.rust.codegen.core.util.orNull
private fun HttpChecksumTrait.requestValidationModeMember(
@ -31,12 +32,16 @@ class HttpResponseChecksumDecorator : ClientCodegenDecorator {
override val name: String = "HttpResponseChecksum"
override val order: Byte = 0
// TODO(enableNewSmithyRuntime): Implement checksumming via interceptor and delete this decorator
private fun applies(codegenContext: ClientCodegenContext): Boolean =
!codegenContext.settings.codegenConfig.enableNewSmithyRuntime
override fun operationCustomizations(
codegenContext: ClientCodegenContext,
operation: OperationShape,
baseCustomizations: List<OperationCustomization>,
): List<OperationCustomization> {
return baseCustomizations + HttpResponseChecksumCustomization(codegenContext, operation)
): List<OperationCustomization> = baseCustomizations.letIf(applies(codegenContext)) {
it + HttpResponseChecksumCustomization(codegenContext, operation)
}
}

View File

@ -103,21 +103,21 @@ class S3ProtocolOverride(codegenContext: CodegenContext) : RestXml(codegenContex
override fun parseHttpErrorMetadata(operationShape: OperationShape): RuntimeType {
return ProtocolFunctions.crossOperationFn("parse_http_error_metadata") { fnName ->
rustBlockTemplate(
"pub fn $fnName(response: &#{Response}<#{Bytes}>) -> Result<#{ErrorBuilder}, #{XmlDecodeError}>",
"pub fn $fnName(response_status: u16, _response_headers: &#{HeaderMap}, response_body: &[u8]) -> Result<#{ErrorBuilder}, #{XmlDecodeError}>",
*errorScope,
) {
rustTemplate(
"""
// S3 HEAD responses have no response body to for an error code. Therefore,
// check the HTTP response status and populate an error code for 404s.
if response.body().is_empty() {
if response_body.is_empty() {
let mut builder = #{ErrorMetadata}::builder();
if response.status().as_u16() == 404 {
if response_status == 404 {
builder = builder.code("NotFound");
}
Ok(builder)
} else {
#{base_errors}::parse_error_metadata(response.body().as_ref())
#{base_errors}::parse_error_metadata(response_body)
}
""",
*errorScope,

View File

@ -100,7 +100,8 @@ fun generateSmithyBuild(services: AwsServices): String {
"includeFluentClient": false,
"renameErrors": false,
"eventStreamAllowList": [$eventStreamAllowListMembers],
"enableNewCrateOrganizationScheme": true
"enableNewCrateOrganizationScheme": true,
"enableNewSmithyRuntime": false
},
"service": "${service.service}",
"module": "$moduleName",

View File

@ -3,10 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
use aws_smithy_http::body::SdkBody;
use aws_smithy_runtime::{AuthOrchestrator, BoxError};
use aws_smithy_runtime_api::client::orchestrator::BoxError;
use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin;
use aws_smithy_runtime_api::config_bag::ConfigBag;
use aws_smithy_runtime_api::runtime_plugin::RuntimePlugin;
#[derive(Debug)]
pub struct GetObjectAuthOrc {}
@ -19,41 +18,7 @@ impl GetObjectAuthOrc {
impl RuntimePlugin for GetObjectAuthOrc {
fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> {
todo!()
}
}
impl AuthOrchestrator<http::Request<SdkBody>> for GetObjectAuthOrc {
fn auth_request(
&self,
_req: &mut http::Request<SdkBody>,
_cfg: &ConfigBag,
) -> Result<(), BoxError> {
todo!()
// let signer = SigV4Signer::new();
// let operation_config = props
// .get::<OperationSigningConfig>()
// .ok_or("missing signing config".to_string())?;
//
// let (operation_config, request_config, creds) = match &operation_config.signing_requirements
// {
// SigningRequirements::Disabled => return Ok(()),
// SigningRequirements::Optional => {
// match aws_sig_auth::middleware::signing_config(props) {
// Ok(parts) => parts,
// Err(_) => return Ok(()),
// }
// }
// SigningRequirements::Required => {
// aws_sig_auth::middleware::signing_config(props).map_err(Box::new)?
// }
// };
//
// let _signature = signer
// .sign(&operation_config, &request_config, &creds, req)
// .expect("signing goes just fine");
//
// Ok(())
// TODO(orchestrator) put an auth orchestrator in the bag
Ok(())
}
}

View File

@ -6,9 +6,11 @@
use aws_smithy_client::conns::Https;
use aws_smithy_client::hyper_ext::Adapter;
use aws_smithy_http::body::SdkBody;
use aws_smithy_runtime::{BoxError, BoxFallibleFut, Connection};
use aws_smithy_runtime_api::client::orchestrator::{
BoxError, BoxFallibleFut, Connection, HttpRequest,
};
use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin;
use aws_smithy_runtime_api::config_bag::ConfigBag;
use aws_smithy_runtime_api::runtime_plugin::RuntimePlugin;
#[derive(Debug)]
pub struct HyperConnection {
@ -17,7 +19,8 @@ pub struct HyperConnection {
impl RuntimePlugin for HyperConnection {
fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> {
todo!()
// TODO(orchestrator) put a connection in the bag
Ok(())
}
}
@ -29,10 +32,10 @@ impl HyperConnection {
}
}
impl Connection<http::Request<SdkBody>, http::Response<SdkBody>> for HyperConnection {
impl Connection for HyperConnection {
fn call(
&self,
_req: &mut http::Request<SdkBody>,
_req: &mut HttpRequest,
_cfg: &ConfigBag,
) -> BoxFallibleFut<http::Response<SdkBody>> {
todo!("hyper's connector wants to take ownership of req");

View File

@ -3,11 +3,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
use aws_sdk_s3::operation::get_object::GetObjectOutput;
use aws_smithy_http::body::SdkBody;
use aws_smithy_runtime::{BoxError, ResponseDeserializer};
use aws_smithy_runtime_api::client::interceptors::context::OutputOrError;
use aws_smithy_runtime_api::client::orchestrator::{BoxError, HttpResponse, ResponseDeserializer};
use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin;
use aws_smithy_runtime_api::config_bag::ConfigBag;
use aws_smithy_runtime_api::runtime_plugin::RuntimePlugin;
#[derive(Debug)]
pub struct GetObjectResponseDeserializer {}
@ -20,356 +19,17 @@ impl GetObjectResponseDeserializer {
impl RuntimePlugin for GetObjectResponseDeserializer {
fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> {
todo!()
// TODO(orchestrator) put a deserializer in the bag
Ok(())
}
}
impl ResponseDeserializer<http::Response<SdkBody>, GetObjectOutput>
for GetObjectResponseDeserializer
{
fn deserialize_response(
&self,
_res: &mut http::Response<SdkBody>,
_cfg: &ConfigBag,
) -> Result<GetObjectOutput, BoxError> {
impl ResponseDeserializer for GetObjectResponseDeserializer {
fn deserialize_streaming(&self, _response: &mut HttpResponse) -> Option<OutputOrError> {
todo!()
}
fn deserialize_nonstreaming(&self, _response: &HttpResponse) -> OutputOrError {
todo!()
// Ok({
// #[allow(unused_mut)]
// let mut output = aws_sdk_s3::output::get_object_output::Builder::default();
// let _ = res;
// output = output.set_accept_ranges(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_accept_ranges(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse AcceptRanges from header `accept-ranges",
// )
// })?,
// );
// output = output.set_body(Some(
// aws_sdk_s3::http_serde::deser_payload_get_object_get_object_output_body(
// res.body_mut(),
// )?,
// ));
// output = output.set_bucket_key_enabled(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_bucket_key_enabled(res.headers())
// .map_err(|_| GetObjectError::unhandled("Failed to parse BucketKeyEnabled from header `x-amz-server-side-encryption-bucket-key-enabled"))?
// );
// output = output.set_cache_control(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_cache_control(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse CacheControl from header `Cache-Control",
// )
// })?,
// );
// output = output.set_checksum_crc32(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_checksum_crc32(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ChecksumCRC32 from header `x-amz-checksum-crc32",
// )
// })?,
// );
// output = output.set_checksum_crc32_c(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_checksum_crc32_c(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ChecksumCRC32C from header `x-amz-checksum-crc32c",
// )
// })?,
// );
// output = output.set_checksum_sha1(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_checksum_sha1(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ChecksumSHA1 from header `x-amz-checksum-sha1",
// )
// })?,
// );
// output = output.set_checksum_sha256(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_checksum_sha256(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ChecksumSHA256 from header `x-amz-checksum-sha256",
// )
// })?,
// );
// output = output.set_content_disposition(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_content_disposition(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ContentDisposition from header `Content-Disposition",
// )
// })?,
// );
// output = output.set_content_encoding(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_content_encoding(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ContentEncoding from header `Content-Encoding",
// )
// })?,
// );
// output = output.set_content_language(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_content_language(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ContentLanguage from header `Content-Language",
// )
// })?,
// );
// output = output.set_content_length(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_content_length(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ContentLength from header `Content-Length",
// )
// })?,
// );
// output = output.set_content_range(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_content_range(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ContentRange from header `Content-Range",
// )
// })?,
// );
// output = output.set_content_type(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_content_type(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ContentType from header `Content-Type",
// )
// })?,
// );
// output = output.set_delete_marker(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_delete_marker(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse DeleteMarker from header `x-amz-delete-marker",
// )
// })?,
// );
// output = output.set_e_tag(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_e_tag(
// res.headers(),
// )
// .map_err(|_| GetObjectError::unhandled("Failed to parse ETag from header `ETag"))?,
// );
// output = output.set_expiration(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_expiration(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse Expiration from header `x-amz-expiration",
// )
// })?,
// );
// output = output.set_expires(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_expires(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled("Failed to parse Expires from header `Expires")
// })?,
// );
// output = output.set_last_modified(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_last_modified(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse LastModified from header `Last-Modified",
// )
// })?,
// );
// output = output.set_metadata(
// aws_sdk_s3::http_serde::deser_prefix_header_get_object_get_object_output_metadata(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse Metadata from prefix header `x-amz-meta-",
// )
// })?,
// );
// output = output.set_missing_meta(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_missing_meta(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse MissingMeta from header `x-amz-missing-meta",
// )
// })?,
// );
// output = output.set_object_lock_legal_hold_status(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_object_lock_legal_hold_status(res.headers())
// .map_err(|_| GetObjectError::unhandled("Failed to parse ObjectLockLegalHoldStatus from header `x-amz-object-lock-legal-hold"))?
// );
// output = output.set_object_lock_mode(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_object_lock_mode(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ObjectLockMode from header `x-amz-object-lock-mode",
// )
// })?,
// );
// output = output.set_object_lock_retain_until_date(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_object_lock_retain_until_date(res.headers())
// .map_err(|_| GetObjectError::unhandled("Failed to parse ObjectLockRetainUntilDate from header `x-amz-object-lock-retain-until-date"))?
// );
// output = output.set_parts_count(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_parts_count(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse PartsCount from header `x-amz-mp-parts-count",
// )
// })?,
// );
// output = output.set_replication_status(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_replication_status(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse ReplicationStatus from header `x-amz-replication-status",
// )
// })?,
// );
// output = output.set_request_charged(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_request_charged(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse RequestCharged from header `x-amz-request-charged",
// )
// })?,
// );
// output = output.set_restore(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_restore(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled("Failed to parse Restore from header `x-amz-restore")
// })?,
// );
// output = output.set_sse_customer_algorithm(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_sse_customer_algorithm(res.headers())
// .map_err(|_| GetObjectError::unhandled("Failed to parse SSECustomerAlgorithm from header `x-amz-server-side-encryption-customer-algorithm"))?
// );
// output = output.set_sse_customer_key_md5(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_sse_customer_key_md5(res.headers())
// .map_err(|_| GetObjectError::unhandled("Failed to parse SSECustomerKeyMD5 from header `x-amz-server-side-encryption-customer-key-MD5"))?
// );
// output = output.set_ssekms_key_id(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_ssekms_key_id(res.headers())
// .map_err(|_| GetObjectError::unhandled("Failed to parse SSEKMSKeyId from header `x-amz-server-side-encryption-aws-kms-key-id"))?
// );
// output = output.set_server_side_encryption(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_server_side_encryption(res.headers())
// .map_err(|_| GetObjectError::unhandled("Failed to parse ServerSideEncryption from header `x-amz-server-side-encryption"))?
// );
// output = output.set_storage_class(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_storage_class(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse StorageClass from header `x-amz-storage-class",
// )
// })?,
// );
// output = output.set_tag_count(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_tag_count(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse TagCount from header `x-amz-tagging-count",
// )
// })?,
// );
// output = output.set_version_id(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_version_id(
// res.headers(),
// )
// .map_err(|_| {
// GetObjectError::unhandled(
// "Failed to parse VersionId from header `x-amz-version-id",
// )
// })?,
// );
// output = output.set_website_redirect_location(
// aws_sdk_s3::http_serde::deser_header_get_object_get_object_output_website_redirect_location(res.headers())
// .map_err(|_| GetObjectError::unhandled("Failed to parse WebsiteRedirectLocation from header `x-amz-website-redirect-location"))?
// );
// output._set_extended_request_id(
// aws_sdk_s3::s3_request_id::RequestIdExt::extended_request_id(res)
// .map(str::to_string),
// );
// output._set_request_id(
// aws_http::request_id::RequestId::request_id(res).map(str::to_string),
// );
// let response_algorithms = ["crc32", "crc32c", "sha256", "sha1"].as_slice();
// let checksum_mode = cfg.get::<aws_sdk_s3::model::ChecksumMode>();
// // Per [the spec](https://awslabs.github.io/smithy/1.0/spec/aws/aws-core.html#http-response-checksums),
// // we check to see if it's the `ENABLED` variant
// if matches!(
// checksum_mode,
// Some(&aws_sdk_s3::model::ChecksumMode::Enabled)
// ) {
// if let Some((checksum_algorithm, precalculated_checksum)) =
// aws_sdk_s3::http_body_checksum::check_headers_for_precalculated_checksum(
// res.headers(),
// response_algorithms,
// )
// {
// let bytestream = output.body.take().map(|bytestream| {
// bytestream.map(move |sdk_body| {
// aws_sdk_s3::http_body_checksum::wrap_body_with_checksum_validator(
// sdk_body,
// checksum_algorithm,
// precalculated_checksum.clone(),
// )
// })
// });
// output = output.set_body(bytestream);
// }
// }
// output.build()
// })
}
}

View File

@ -3,10 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
use aws_smithy_http::body::SdkBody;
use aws_smithy_runtime::{BoxError, EndpointOrchestrator};
use aws_smithy_http::event_stream::BoxError;
use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin;
use aws_smithy_runtime_api::config_bag::ConfigBag;
use aws_smithy_runtime_api::runtime_plugin::RuntimePlugin;
#[derive(Debug)]
pub struct GetObjectEndpointOrc {}
@ -19,86 +18,7 @@ impl GetObjectEndpointOrc {
impl RuntimePlugin for GetObjectEndpointOrc {
fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> {
todo!()
}
}
impl EndpointOrchestrator<http::Request<SdkBody>> for GetObjectEndpointOrc {
fn resolve_and_apply_endpoint(
&self,
_req: &mut http::Request<SdkBody>,
_cfg: &ConfigBag,
) -> Result<(), BoxError> {
todo!()
// let endpoint = endpoint_resolver.resolve_endpoint(&endpoint_parameters)?;
// let (tx_req, props) = ctx
// .tx_request_mut()
// .expect("We call this after setting the tx request");
//
// // Apply the endpoint
// let uri: Uri = endpoint.url().parse().map_err(|err| {
// ResolveEndpointError::from_source("endpoint did not have a valid uri", err)
// })?;
// apply_endpoint(tx_req.uri_mut(), &uri, props.get::<EndpointPrefix>()).map_err(|err| {
// ResolveEndpointError::message(format!(
// "failed to apply endpoint `{:?}` to request `{:?}`",
// uri, tx_req
// ))
// .with_source(Some(err.into()))
// })?;
// for (header_name, header_values) in endpoint.headers() {
// tx_req.headers_mut().remove(header_name);
// for value in header_values {
// tx_req.headers_mut().insert(
// HeaderName::from_str(header_name).map_err(|err| {
// ResolveEndpointError::message("invalid header name")
// .with_source(Some(err.into()))
// })?,
// HeaderValue::from_str(value).map_err(|err| {
// ResolveEndpointError::message("invalid header value")
// .with_source(Some(err.into()))
// })?,
// );
// }
// }
}
fn resolve_auth_schemes(&self) -> Result<Vec<String>, BoxError> {
todo!()
// let endpoint = endpoint_resolver
// .resolve_endpoint(params)
// .map_err(SdkError::construction_failure)?;
// let auth_schemes = match endpoint.properties().get("authSchemes") {
// Some(Document::Array(schemes)) => schemes,
// None => {
// return Ok(vec![]);
// }
// _other => {
// return Err(SdkError::construction_failure(
// "expected bad things".to_string(),
// ));
// }
// };
// let auth_schemes = auth_schemes
// .iter()
// .flat_map(|doc| match doc {
// Document::Object(map) => Some(map),
// _ => None,
// })
// .map(|it| {
// let name = match it.get("name") {
// Some(Document::String(s)) => Some(s.as_str()),
// _ => None,
// };
// AuthSchemeOptions::new(
// name.unwrap().to_string(),
// /* there are no identity properties yet */
// None,
// Some(Document::Object(it.clone())),
// )
// })
// .collect::<Vec<_>>();
// Ok(auth_schemes)
// TODO(orchestrator) put an endpoint orchestrator in the bag
Ok(())
}
}

View File

@ -11,15 +11,14 @@ mod interceptors;
mod retry;
mod ser;
use aws_sdk_s3::operation::get_object::{GetObjectInput, GetObjectOutput};
use aws_sdk_s3::operation::get_object::{GetObjectError, GetObjectInput, GetObjectOutput};
use aws_sdk_s3::types::ChecksumMode;
use aws_smithy_http::body::SdkBody;
use aws_smithy_runtime::{invoke, BoxError};
use aws_smithy_runtime::client::orchestrator::invoke;
use aws_smithy_runtime_api::client::interceptors::Interceptors;
use aws_smithy_runtime_api::client::orchestrator::{BoxError, HttpRequest, HttpResponse};
use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugins;
use aws_smithy_runtime_api::config_bag::ConfigBag;
use aws_smithy_runtime_api::interceptors::Interceptors;
use aws_smithy_runtime_api::runtime_plugin::RuntimePlugins;
use std::str::from_utf8;
use tracing::info;
use aws_smithy_runtime_api::type_erasure::TypedBox;
#[tokio::main]
async fn main() -> Result<(), BoxError> {
@ -29,11 +28,14 @@ async fn main() -> Result<(), BoxError> {
let sdk_config = aws_config::load_from_env().await;
let _service_config = aws_sdk_s3::Config::from(&sdk_config);
let input = GetObjectInput::builder()
let input = TypedBox::new(
GetObjectInput::builder()
.bucket("zhessler-test-bucket")
.key("1000-lines.txt")
.checksum_mode(ChecksumMode::Enabled)
.build()?;
.build()?,
)
.erase();
let mut runtime_plugins = RuntimePlugins::new();
@ -48,18 +50,21 @@ async fn main() -> Result<(), BoxError> {
.with_operation_plugin(ser::GetObjectInputSerializer::new());
let mut cfg = ConfigBag::base();
let mut interceptors: Interceptors<
GetObjectInput,
http::Request<SdkBody>,
http::Response<SdkBody>,
Result<GetObjectOutput, BoxError>,
> = Interceptors::new();
let res = invoke(input, &mut interceptors, &runtime_plugins, &mut cfg).await?;
let body = res.body.collect().await?.to_vec();
let body_string = from_utf8(&body)?;
info!("{body_string}");
let mut interceptors: Interceptors<HttpRequest, HttpResponse> = Interceptors::new();
let output = TypedBox::<GetObjectOutput>::assume_from(
invoke(input, &mut interceptors, &runtime_plugins, &mut cfg)
.await
.map_err(|err| {
err.map_service_error(|err| {
TypedBox::<GetObjectError>::assume_from(err)
.expect("error is GetObjectError")
.unwrap()
})
})?,
)
.expect("output is GetObjectOutput")
.unwrap();
dbg!(output);
Ok(())
}

View File

@ -3,10 +3,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
use aws_sdk_s3::operation::get_object::{GetObjectError, GetObjectOutput};
use aws_smithy_runtime::{BoxError, RetryStrategy};
use aws_smithy_runtime_api::client::interceptors::InterceptorContext;
use aws_smithy_runtime_api::client::orchestrator::{
BoxError, HttpRequest, HttpResponse, RetryStrategy,
};
use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin;
use aws_smithy_runtime_api::config_bag::ConfigBag;
use aws_smithy_runtime_api::runtime_plugin::RuntimePlugin;
#[derive(Debug)]
pub struct GetObjectRetryStrategy {}
@ -19,14 +21,19 @@ impl GetObjectRetryStrategy {
impl RuntimePlugin for GetObjectRetryStrategy {
fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> {
todo!()
// TODO(orchestrator) put a retry strategy in the bag
Ok(())
}
}
impl RetryStrategy<Result<GetObjectOutput, GetObjectError>> for GetObjectRetryStrategy {
fn should_retry(
impl RetryStrategy for GetObjectRetryStrategy {
fn should_attempt_initial_request(&self, _cfg: &ConfigBag) -> Result<(), BoxError> {
todo!()
}
fn should_attempt_retry(
&self,
_res: &Result<GetObjectOutput, GetObjectError>,
_context: &InterceptorContext<HttpRequest, HttpResponse>,
_cfg: &ConfigBag,
) -> Result<bool, BoxError> {
todo!()

View File

@ -3,11 +3,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
use aws_sdk_s3::operation::get_object::GetObjectInput;
use aws_smithy_http::body::SdkBody;
use aws_smithy_runtime::{BoxError, RequestSerializer};
use aws_smithy_http::event_stream::BoxError;
use aws_smithy_runtime_api::client::interceptors::context::Input;
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, RequestSerializer};
use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin;
use aws_smithy_runtime_api::config_bag::ConfigBag;
use aws_smithy_runtime_api::runtime_plugin::RuntimePlugin;
#[derive(Debug)]
pub struct GetObjectInputSerializer {}
@ -20,125 +20,13 @@ impl GetObjectInputSerializer {
impl RuntimePlugin for GetObjectInputSerializer {
fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> {
todo!()
// TODO(orchestrator) put a serializer in the bag
Ok(())
}
}
impl RequestSerializer<GetObjectInput, http::Request<SdkBody>> for GetObjectInputSerializer {
fn serialize_request(
&self,
_input: &mut GetObjectInput,
_cfg: &ConfigBag,
) -> Result<http::Request<SdkBody>, BoxError> {
impl RequestSerializer for GetObjectInputSerializer {
fn serialize_input(&self, _input: &Input, _cfg: &ConfigBag) -> Result<HttpRequest, BoxError> {
todo!()
// let request = {
// fn uri_base(_input: &GetObjectInput, output: &mut String) -> Result<(), BuildError> {
// use std::fmt::Write;
//
// let input_30 = &_input.key;
// let input_30 = input_30
// .as_ref()
// .ok_or_else(|| BuildError::missing_field("key", "cannot be empty or unset"))?;
// let key = aws_smithy_http::label::fmt_string(
// input_30,
// aws_smithy_http::label::EncodingStrategy::Greedy,
// );
// if key.is_empty() {
// return Err(BuildError::missing_field("key", "cannot be empty or unset"));
// }
// write!(output, "/{Key}", Key = key).expect("formatting should succeed");
// Ok(())
// }
// fn uri_query(
// _input: &GetObjectInput,
// mut output: &mut String,
// ) -> Result<(), BuildError> {
// let mut query = aws_smithy_http::query::Writer::new(&mut output);
// query.push_kv("x-id", "GetObject");
// if let Some(inner_31) = &_input.response_cache_control {
// {
// query.push_kv(
// "response-cache-control",
// &aws_smithy_http::query::fmt_string(&inner_31),
// );
// }
// }
// if let Some(inner_32) = &_input.response_content_disposition {
// {
// query.push_kv(
// "response-content-disposition",
// &aws_smithy_http::query::fmt_string(&inner_32),
// );
// }
// }
// if let Some(inner_33) = &_input.response_content_encoding {
// {
// query.push_kv(
// "response-content-encoding",
// &aws_smithy_http::query::fmt_string(&inner_33),
// );
// }
// }
// if let Some(inner_34) = &_input.response_content_language {
// {
// query.push_kv(
// "response-content-language",
// &aws_smithy_http::query::fmt_string(&inner_34),
// );
// }
// }
// if let Some(inner_35) = &_input.response_content_type {
// {
// query.push_kv(
// "response-content-type",
// &aws_smithy_http::query::fmt_string(&inner_35),
// );
// }
// }
// if let Some(inner_36) = &_input.response_expires {
// {
// query.push_kv(
// "response-expires",
// &aws_smithy_http::query::fmt_timestamp(
// inner_36,
// aws_smithy_types::date_time::Format::HttpDate,
// )?,
// );
// }
// }
// if let Some(inner_37) = &_input.version_id {
// {
// query.push_kv("versionId", &aws_smithy_http::query::fmt_string(&inner_37));
// }
// }
// if let Some(inner_38) = &_input.part_number {
// if *inner_38 != 0 {
// query.push_kv(
// "partNumber",
// aws_smithy_types::primitive::Encoder::from(*inner_38).encode(),
// );
// }
// }
// Ok(())
// }
//
// fn update_http_builder(
// input: &GetObjectInput,
// builder: http::request::Builder,
// ) -> Result<http::request::Builder, BuildError> {
// let mut uri = String::new();
// uri_base(input, &mut uri)?;
// uri_query(input, &mut uri)?;
// let builder = aws_sdk_s3::http_serde::add_headers_get_object(input, builder)?;
// Ok(builder.method("GET").uri(uri))
// }
// let builder = update_http_builder(&input, http::request::Builder::new())?;
// builder
// };
//
// let _properties = aws_smithy_http::property_bag::SharedPropertyBag::new();
// #[allow(clippy::useless_conversion)]
// let body = aws_smithy_http::body::SdkBody::from("");
// Ok(request.body(body).expect("should be valid request"))
}
}

1
aws/sra-test/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/smithy-build.json

View File

@ -0,0 +1,88 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
extra["displayName"] = "Smithy :: Rust :: AWS-SDK :: SRA Test"
extra["moduleName"] = "software.amazon.smithy.rust.awssdk.sra.test"
tasks["jar"].enabled = false
plugins {
id("software.amazon.smithy")
}
val smithyVersion: String by project
val defaultRustDocFlags: String by project
val properties = PropertyRetriever(rootProject, project)
val pluginName = "rust-client-codegen"
val workingDirUnderBuildDir = "smithyprojections/sdk-sra-test/"
configure<software.amazon.smithy.gradle.SmithyExtension> {
outputDirectory = file("$buildDir/$workingDirUnderBuildDir")
}
buildscript {
val smithyVersion: String by project
dependencies {
classpath("software.amazon.smithy:smithy-cli:$smithyVersion")
}
}
dependencies {
implementation(project(":aws:sdk-codegen"))
implementation("software.amazon.smithy:smithy-protocol-test-traits:$smithyVersion")
implementation("software.amazon.smithy:smithy-aws-traits:$smithyVersion")
}
val allCodegenTests = listOf(
CodegenTest(
"com.amazonaws.dynamodb#DynamoDB_20120810",
"aws-sdk-dynamodb",
imports = listOf("../sdk/aws-models/dynamodb.json"),
extraConfig = """
,
"codegen": {
"includeFluentClient": false,
"enableNewSmithyRuntime": true
},
"customizationConfig": {
"awsSdk": {
"generateReadme": false
}
}
""",
),
CodegenTest(
"com.amazonaws.s3#AmazonS3",
"aws-sdk-s3",
imports = listOf("../sdk/aws-models/s3.json", "../sdk/aws-models/s3-tests.smithy"),
extraConfig = """
,
"codegen": {
"includeFluentClient": false,
"enableNewSmithyRuntime": true
},
"customizationConfig": {
"awsSdk": {
"generateReadme": false
}
}
""",
),
)
project.registerGenerateSmithyBuildTask(rootProject, pluginName, allCodegenTests)
project.registerGenerateCargoWorkspaceTask(rootProject, pluginName, allCodegenTests, workingDirUnderBuildDir)
project.registerGenerateCargoConfigTomlTask(buildDir.resolve(workingDirUnderBuildDir))
tasks["smithyBuildJar"].dependsOn("generateSmithyBuild")
tasks["assemble"].finalizedBy("generateCargoWorkspace")
project.registerModifyMtimeTask()
project.registerCargoCommandsTasks(buildDir.resolve(workingDirUnderBuildDir), defaultRustDocFlags)
tasks["test"].finalizedBy(cargoCommands(properties).map { it.toString })
tasks["clean"].doFirst { delete("smithy-build.json") }

View File

@ -88,6 +88,8 @@ data class ClientCodegenConfig(
val eventStreamAllowList: Set<String> = defaultEventStreamAllowList,
// TODO(CrateReorganization): Remove this once we commit to the breaking change
val enableNewCrateOrganizationScheme: Boolean = defaultEnableNewCrateOrganizationScheme,
// TODO(SmithyRuntime): Remove this once we commit to switch to aws-smithy-runtime and aws-smithy-runtime-api
val enableNewSmithyRuntime: Boolean = defaultEnableNewSmithyRuntime,
) : CoreCodegenConfig(
formatTimeoutSeconds, debugMode,
) {
@ -97,6 +99,7 @@ data class ClientCodegenConfig(
private const val defaultAddMessageToErrors = true
private val defaultEventStreamAllowList: Set<String> = emptySet()
private const val defaultEnableNewCrateOrganizationScheme = true
private const val defaultEnableNewSmithyRuntime = false
fun fromCodegenConfigAndNode(coreCodegenConfig: CoreCodegenConfig, node: Optional<ObjectNode>) =
if (node.isPresent) {
@ -110,13 +113,12 @@ data class ClientCodegenConfig(
includeFluentClient = node.get().getBooleanMemberOrDefault("includeFluentClient", defaultIncludeFluentClient),
addMessageToErrors = node.get().getBooleanMemberOrDefault("addMessageToErrors", defaultAddMessageToErrors),
enableNewCrateOrganizationScheme = node.get().getBooleanMemberOrDefault("enableNewCrateOrganizationScheme", defaultEnableNewCrateOrganizationScheme),
enableNewSmithyRuntime = node.get().getBooleanMemberOrDefault("enableNewSmithyRuntime", defaultEnableNewSmithyRuntime),
)
} else {
ClientCodegenConfig(
formatTimeoutSeconds = coreCodegenConfig.formatTimeoutSeconds,
debugMode = coreCodegenConfig.debugMode,
eventStreamAllowList = defaultEventStreamAllowList,
enableNewCrateOrganizationScheme = defaultEnableNewCrateOrganizationScheme,
)
}
}

View File

@ -14,6 +14,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.generators.http.Respons
import software.amazon.smithy.rust.codegen.client.smithy.generators.protocol.ClientProtocolGenerator
import software.amazon.smithy.rust.codegen.client.smithy.generators.protocol.MakeOperationGenerator
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
import software.amazon.smithy.rust.codegen.core.rustlang.assignment
@ -23,7 +24,6 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.core.rustlang.withBlock
import software.amazon.smithy.rust.codegen.core.rustlang.writable
import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationCustomization
import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationSection
@ -63,7 +63,7 @@ class HttpBoundProtocolGenerator(
)
class HttpBoundProtocolTraitImplGenerator(
private val codegenContext: CodegenContext,
private val codegenContext: ClientCodegenContext,
private val protocol: Protocol,
) : ProtocolTraitImplGenerator {
private val symbolProvider = codegenContext.symbolProvider
@ -78,7 +78,27 @@ class HttpBoundProtocolTraitImplGenerator(
"http" to RuntimeType.Http,
"operation" to RuntimeType.operationModule(runtimeConfig),
"Bytes" to RuntimeType.Bytes,
"SdkBody" to RuntimeType.sdkBody(runtimeConfig),
)
private val orchestratorCodegenScope by lazy {
val interceptorContext =
CargoDependency.smithyRuntimeApi(runtimeConfig).toType().resolve("client::interceptors::context")
val orchestrator =
CargoDependency.smithyRuntimeApi(runtimeConfig).toType().resolve("client::orchestrator")
arrayOf(
"Error" to interceptorContext.resolve("Error"),
"HttpResponse" to orchestrator.resolve("HttpResponse"),
"Instrument" to CargoDependency.Tracing.toType().resolve("Instrument"),
"Output" to interceptorContext.resolve("Output"),
"OutputOrError" to interceptorContext.resolve("OutputOrError"),
"ResponseDeserializer" to orchestrator.resolve("ResponseDeserializer"),
"SdkBody" to RuntimeType.sdkBody(runtimeConfig),
"SdkError" to RuntimeType.sdkError(runtimeConfig),
"TypedBox" to CargoDependency.smithyRuntimeApi(runtimeConfig).toType().resolve("type_erasure::TypedBox"),
"debug_span" to RuntimeType.Tracing.resolve("debug_span"),
"type_erase_result" to typeEraseResult(),
)
}
override fun generateTraitImpls(
operationWriter: RustWriter,
@ -91,40 +111,127 @@ class HttpBoundProtocolTraitImplGenerator(
// For streaming response bodies, we need to generate a different implementation of the parse traits.
// These will first offer the streaming input to the parser & potentially read the body into memory
// if an error occurred or if the streaming parser indicates that it needs the full data to proceed.
if (operationShape.outputShape(model).hasStreamingMember(model)) {
with(operationWriter) {
renderStreamingTraits(operationName, outputSymbol, operationShape, customizations)
}
val streaming = operationShape.outputShape(model).hasStreamingMember(model)
if (streaming) {
operationWriter.renderStreamingTraits(operationName, outputSymbol, operationShape, customizations)
} else {
with(operationWriter) {
renderNonStreamingTraits(operationName, outputSymbol, operationShape, customizations)
operationWriter.renderNonStreamingTraits(operationName, outputSymbol, operationShape, customizations)
}
if (codegenContext.settings.codegenConfig.enableNewSmithyRuntime) {
operationWriter.renderRuntimeTraits(operationName, outputSymbol, operationShape, customizations, streaming)
}
}
private fun RustWriter.renderNonStreamingTraits(
private fun typeEraseResult(): RuntimeType = ProtocolFunctions.crossOperationFn("type_erase_result") { fnName ->
rustTemplate(
"""
pub(crate) fn $fnName<O, E>(result: Result<O, E>) -> Result<#{Output}, #{Error}>
where
O: Send + Sync + 'static,
E: Send + Sync + 'static,
{
result.map(|output| #{TypedBox}::new(output).erase())
.map_err(|error| #{TypedBox}::new(error).erase())
}
""",
*orchestratorCodegenScope,
)
}
private fun RustWriter.renderRuntimeTraits(
operationName: String?,
outputSymbol: Symbol,
operationShape: OperationShape,
customizations: List<OperationCustomization>,
streaming: Boolean,
) {
rustTemplate(
"""
impl #{ResponseDeserializer} for $operationName {
#{deserialize_streaming}
fn deserialize_nonstreaming(&self, response: &#{HttpResponse}) -> #{OutputOrError} {
#{deserialize_nonstreaming}
}
}
""",
*orchestratorCodegenScope,
"O" to outputSymbol,
"E" to symbolProvider.symbolForOperationError(operationShape),
"deserialize_streaming" to writable {
if (streaming) {
deserializeStreaming(operationShape, customizations)
}
},
"deserialize_nonstreaming" to writable {
when (streaming) {
true -> deserializeStreamingError(operationShape, customizations)
else -> deserializeNonStreaming(operationShape, customizations)
}
},
)
}
private fun RustWriter.deserializeStreaming(
operationShape: OperationShape,
customizations: List<OperationCustomization>,
) {
val successCode = httpBindingResolver.httpTrait(operationShape).code
rustTemplate(
"""
impl #{ParseStrict} for $operationName {
type Output = std::result::Result<#{O}, #{E}>;
fn parse(&self, response: &#{http}::Response<#{Bytes}>) -> Self::Output {
fn deserialize_streaming(&self, response: &mut #{HttpResponse}) -> Option<#{OutputOrError}> {
#{BeforeParseResponse}
// If this is an error, defer to the non-streaming parser
if !response.status().is_success() && response.status().as_u16() != $successCode {
#{parse_error}(response)
return None;
}
Some(#{type_erase_result}(#{parse_streaming_response}(response)))
}
""",
*orchestratorCodegenScope,
"parse_streaming_response" to parseStreamingResponse(operationShape, customizations),
"BeforeParseResponse" to writable {
writeCustomizations(customizations, OperationSection.BeforeParseResponse(customizations, "response"))
},
)
}
private fun RustWriter.deserializeStreamingError(
operationShape: OperationShape,
customizations: List<OperationCustomization>,
) {
rustTemplate(
"""
// For streaming operations, we only hit this case if its an error
let body = response.body().bytes().expect("body loaded");
#{type_erase_result}(#{parse_error}(response.status().as_u16(), response.headers(), body))
""",
*orchestratorCodegenScope,
"parse_error" to parseError(operationShape, customizations),
)
}
private fun RustWriter.deserializeNonStreaming(
operationShape: OperationShape,
customizations: List<OperationCustomization>,
) {
val successCode = httpBindingResolver.httpTrait(operationShape).code
rustTemplate(
"""
let (success, status) = (response.status().is_success(), response.status().as_u16());
let headers = response.headers();
let body = response.body().bytes().expect("body loaded");
#{BeforeParseResponse}
let parse_result = if !success && status != $successCode {
#{parse_error}(status, headers, body)
} else {
#{parse_response}(response)
}
}
}""",
*codegenScope,
"O" to outputSymbol,
"E" to symbolProvider.symbolForOperationError(operationShape),
#{parse_response}(status, headers, body)
};
#{type_erase_result}(parse_result)
""",
*orchestratorCodegenScope,
"parse_error" to parseError(operationShape, customizations),
"parse_response" to parseResponse(operationShape, customizations),
"BeforeParseResponse" to writable {
@ -133,6 +240,45 @@ class HttpBoundProtocolTraitImplGenerator(
)
}
// TODO(enableNewSmithyRuntime): Delete this when cleaning up `enableNewSmithyRuntime`
private fun RustWriter.renderNonStreamingTraits(
operationName: String?,
outputSymbol: Symbol,
operationShape: OperationShape,
customizations: List<OperationCustomization>,
) {
val successCode = httpBindingResolver.httpTrait(operationShape).code
val localScope = arrayOf(
"O" to outputSymbol,
"E" to symbolProvider.symbolForOperationError(operationShape),
"parse_error" to parseError(operationShape, customizations),
"parse_response" to parseResponse(operationShape, customizations),
"BeforeParseResponse" to writable {
writeCustomizations(customizations, OperationSection.BeforeParseResponse(customizations, "response"))
},
)
rustTemplate(
"""
impl #{ParseStrict} for $operationName {
type Output = std::result::Result<#{O}, #{E}>;
fn parse(&self, response: &#{http}::Response<#{Bytes}>) -> Self::Output {
let (success, status) = (response.status().is_success(), response.status().as_u16());
let headers = response.headers();
let body = response.body().as_ref();
#{BeforeParseResponse}
if !success && status != $successCode {
#{parse_error}(status, headers, body)
} else {
#{parse_response}(status, headers, body)
}
}
}""",
*codegenScope,
*localScope,
)
}
// TODO(enableNewSmithyRuntime): Delete this when cleaning up `enableNewSmithyRuntime`
private fun RustWriter.renderStreamingTraits(
operationName: String,
outputSymbol: Symbol,
@ -154,13 +300,13 @@ class HttpBoundProtocolTraitImplGenerator(
}
fn parse_loaded(&self, response: &#{http}::Response<#{Bytes}>) -> Self::Output {
// if streaming, we only hit this case if its an error
#{parse_error}(response)
#{parse_error}(response.status().as_u16(), response.headers(), response.body().as_ref())
}
}
""",
"O" to outputSymbol,
"E" to symbolProvider.symbolForOperationError(operationShape),
"parse_streaming_response" to parseStreamingResponse(operationShape, customizations),
"parse_streaming_response" to parseStreamingResponseNoRt(operationShape, customizations),
"parse_error" to parseError(operationShape, customizations),
"BeforeParseResponse" to writable {
writeCustomizations(customizations, OperationSection.BeforeParseResponse(customizations, "response"))
@ -176,20 +322,25 @@ class HttpBoundProtocolTraitImplGenerator(
return protocolFunctions.deserializeFn(operationShape, fnNameSuffix = "http_error") { fnName ->
Attribute.AllowClippyUnnecessaryWraps.render(this)
rustBlockTemplate(
"pub fn $fnName(response: &#{http}::Response<#{Bytes}>) -> std::result::Result<#{O}, #{E}>",
"pub fn $fnName(_response_status: u16, _response_headers: &#{http}::header::HeaderMap, _response_body: &[u8]) -> std::result::Result<#{O}, #{E}>",
*codegenScope,
"O" to outputSymbol,
"E" to errorSymbol,
) {
Attribute.AllowUnusedMut.render(this)
rust(
"let mut generic_builder = #T(response).map_err(#T::unhandled)?;",
"let mut generic_builder = #T(_response_status, _response_headers, _response_body).map_err(#T::unhandled)?;",
protocol.parseHttpErrorMetadata(operationShape),
errorSymbol,
)
writeCustomizations(
customizations,
OperationSection.PopulateErrorMetadataExtras(customizations, "generic_builder", "response"),
OperationSection.PopulateErrorMetadataExtras(
customizations,
"generic_builder",
"_response_status",
"_response_headers",
),
)
rust("let generic = generic_builder.build();")
if (operationShape.operationErrors(model).isNotEmpty()) {
@ -260,6 +411,43 @@ class HttpBoundProtocolTraitImplGenerator(
val outputSymbol = symbolProvider.toSymbol(outputShape)
val errorSymbol = symbolProvider.symbolForOperationError(operationShape)
return protocolFunctions.deserializeFn(operationShape, fnNameSuffix = "http_response") { fnName ->
Attribute.AllowClippyUnnecessaryWraps.render(this)
rustBlockTemplate(
"pub fn $fnName(response: &mut #{http}::Response<#{SdkBody}>) -> std::result::Result<#{O}, #{E}>",
*codegenScope,
"O" to outputSymbol,
"E" to errorSymbol,
) {
rustTemplate(
"""
let mut _response_body = #{SdkBody}::taken();
std::mem::swap(&mut _response_body, response.body_mut());
let _response_body = &mut _response_body;
let _response_status = response.status().as_u16();
let _response_headers = response.headers();
""",
*codegenScope,
)
withBlock("Ok({", "})") {
renderShapeParser(
operationShape,
outputShape,
httpBindingResolver.responseBindings(operationShape),
errorSymbol,
customizations,
)
}
}
}
}
// TODO(enableNewSmithyRuntime): Delete this when cleaning up `enableNewSmithyRuntime`
private fun parseStreamingResponseNoRt(operationShape: OperationShape, customizations: List<OperationCustomization>): RuntimeType {
val outputShape = operationShape.outputShape(model)
val outputSymbol = symbolProvider.toSymbol(outputShape)
val errorSymbol = symbolProvider.symbolForOperationError(operationShape)
return protocolFunctions.deserializeFn(operationShape, fnNameSuffix = "http_response_") { fnName ->
Attribute.AllowClippyUnnecessaryWraps.render(this)
rustBlockTemplate(
"pub fn $fnName(op_response: &mut #{operation}::Response) -> std::result::Result<#{O}, #{E}>",
@ -270,6 +458,17 @@ class HttpBoundProtocolTraitImplGenerator(
// Not all implementations will use the property bag, but some will
Attribute.AllowUnusedVariables.render(this)
rust("let (response, properties) = op_response.parts_mut();")
rustTemplate(
"""
let mut _response_body = #{SdkBody}::taken();
std::mem::swap(&mut _response_body, response.body_mut());
let _response_body = &mut _response_body;
let _response_status = response.status().as_u16();
let _response_headers = response.headers();
""",
*codegenScope,
)
withBlock("Ok({", "})") {
renderShapeParser(
operationShape,
@ -290,7 +489,7 @@ class HttpBoundProtocolTraitImplGenerator(
return protocolFunctions.deserializeFn(operationShape, fnNameSuffix = "http_response") { fnName ->
Attribute.AllowClippyUnnecessaryWraps.render(this)
rustBlockTemplate(
"pub fn $fnName(response: &#{http}::Response<#{Bytes}>) -> std::result::Result<#{O}, #{E}>",
"pub fn $fnName(_response_status: u16, _response_headers: &#{http}::header::HeaderMap, _response_body: &[u8]) -> std::result::Result<#{O}, #{E}>",
*codegenScope,
"O" to outputSymbol,
"E" to errorSymbol,
@ -319,12 +518,10 @@ class HttpBoundProtocolTraitImplGenerator(
val structuredDataParser = protocol.structuredDataParser(operationShape)
Attribute.AllowUnusedMut.render(this)
rust("let mut output = #T::default();", symbolProvider.symbolForBuilder(outputShape))
// avoid non-usage warnings for response
rust("let _ = response;")
if (outputShape.id == operationShape.output.get()) {
structuredDataParser.operationParser(operationShape)?.also { parser ->
rust(
"output = #T(response.body().as_ref(), output).map_err(#T::unhandled)?;",
"output = #T(_response_body, output).map_err(#T::unhandled)?;",
parser,
errorSymbol,
)
@ -333,7 +530,7 @@ class HttpBoundProtocolTraitImplGenerator(
check(outputShape.hasTrait<ErrorTrait>()) { "should only be called on outputs or errors $outputShape" }
structuredDataParser.errorParser(outputShape)?.also { parser ->
rust(
"output = #T(response.body().as_ref(), output).map_err(#T::unhandled)?;",
"output = #T(_response_body, output).map_err(#T::unhandled)?;",
parser, errorSymbol,
)
}
@ -354,7 +551,10 @@ class HttpBoundProtocolTraitImplGenerator(
""
}
writeCustomizations(customizations, OperationSection.MutateOutput(customizations, operationShape))
writeCustomizations(
customizations,
OperationSection.MutateOutput(customizations, operationShape, "_response_headers"),
)
rust("output.build()$err")
}
@ -377,7 +577,7 @@ class HttpBoundProtocolTraitImplGenerator(
val fnName = httpBindingGenerator.generateDeserializeHeaderFn(binding)
rust(
"""
#T(response.headers())
#T(_response_headers)
.map_err(|_|#T::unhandled("Failed to parse ${member.memberName} from header `${binding.locationName}"))?
""",
fnName, errorSymbol,
@ -397,20 +597,20 @@ class HttpBoundProtocolTraitImplGenerator(
payloadParser = payloadParser,
)
return if (binding.member.isStreaming(model)) {
writable { rust("Some(#T(response.body_mut())?)", deserializer) }
writable { rust("Some(#T(_response_body)?)", deserializer) }
} else {
writable { rust("#T(response.body().as_ref())?", deserializer) }
writable { rust("#T(_response_body)?", deserializer) }
}
}
HttpLocation.RESPONSE_CODE -> writable {
rust("Some(response.status().as_u16() as _)")
rust("Some(_response_status as _)")
}
HttpLocation.PREFIX_HEADERS -> {
val sym = httpBindingGenerator.generateDeserializePrefixHeaderFn(binding)
writable {
rustTemplate(
"""
#{deser}(response.headers())
#{deser}(_response_headers)
.map_err(|_|
#{err}::unhandled("Failed to parse ${member.memberName} from prefix header `${binding.locationName}")
)?

View File

@ -266,6 +266,8 @@ data class CargoDependency(
runtimeConfig.smithyRuntimeCrate("smithy-protocol-test", scope = DependencyScope.Dev)
fun smithyQuery(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-query")
fun smithyRuntime(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-runtime")
fun smithyRuntimeApi(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-runtime-api")
fun smithyTypes(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-types")
fun smithyXml(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-xml")
}

View File

@ -52,6 +52,8 @@ sealed class OperationSection(name: String) : Section(name) {
data class MutateOutput(
override val customizations: List<OperationCustomization>,
val operationShape: OperationShape,
/** Name of the response headers map (for referring to it in Rust code) */
val responseHeadersName: String,
) : OperationSection("MutateOutput")
/**
@ -62,8 +64,10 @@ sealed class OperationSection(name: String) : Section(name) {
override val customizations: List<OperationCustomization>,
/** Name of the generic error builder (for referring to it in Rust code) */
val builderName: String,
/** Name of the response (for referring to it in Rust code) */
val responseName: String,
/** Name of the response status (for referring to it in Rust code) */
val responseStatusName: String,
/** Name of the response headers map (for referring to it in Rust code) */
val responseHeadersName: String,
) : OperationSection("PopulateErrorMetadataExtras")
/**

View File

@ -130,7 +130,6 @@ open class AwsJson(
"HeaderMap" to RuntimeType.Http.resolve("HeaderMap"),
"JsonError" to CargoDependency.smithyJson(runtimeConfig).toType()
.resolve("deserialize::error::DeserializeError"),
"Response" to RuntimeType.Http.resolve("Response"),
"json_errors" to RuntimeType.jsonErrors(runtimeConfig),
)
@ -159,8 +158,8 @@ open class AwsJson(
ProtocolFunctions.crossOperationFn("parse_http_error_metadata") { fnName ->
rustTemplate(
"""
pub fn $fnName(response: &#{Response}<#{Bytes}>) -> Result<#{ErrorMetadataBuilder}, #{JsonError}> {
#{json_errors}::parse_error_metadata(response.body(), response.headers())
pub fn $fnName(_response_status: u16, response_headers: &#{HeaderMap}, response_body: &[u8]) -> Result<#{ErrorMetadataBuilder}, #{JsonError}> {
#{json_errors}::parse_error_metadata(response_body, response_headers)
}
""",
*errorScope,

View File

@ -60,10 +60,10 @@ class AwsQueryProtocol(private val codegenContext: CodegenContext) : Protocol {
override fun parseHttpErrorMetadata(operationShape: OperationShape): RuntimeType =
ProtocolFunctions.crossOperationFn("parse_http_error_metadata") { fnName ->
rustBlockTemplate(
"pub fn $fnName(response: &#{Response}<#{Bytes}>) -> Result<#{ErrorMetadataBuilder}, #{XmlDecodeError}>",
"pub fn $fnName(_response_status: u16, _response_headers: &#{HeaderMap}, response_body: &[u8]) -> Result<#{ErrorMetadataBuilder}, #{XmlDecodeError}>",
*errorScope,
) {
rust("#T::parse_error_metadata(response.body().as_ref())", awsQueryErrors)
rust("#T::parse_error_metadata(response_body)", awsQueryErrors)
}
}

View File

@ -49,11 +49,11 @@ class AwsQueryCompatible(
private val errorScope = arrayOf(
"Bytes" to RuntimeType.Bytes,
"ErrorMetadataBuilder" to RuntimeType.errorMetadataBuilder(runtimeConfig),
"HeaderMap" to RuntimeType.HttpHeaderMap,
"JsonError" to CargoDependency.smithyJson(runtimeConfig).toType()
.resolve("deserialize::error::DeserializeError"),
"Response" to RuntimeType.Http.resolve("Response"),
"json_errors" to RuntimeType.jsonErrors(runtimeConfig),
"aws_query_compatible_errors" to RuntimeType.awsQueryCompatibleErrors(runtimeConfig),
"json_errors" to RuntimeType.jsonErrors(runtimeConfig),
)
override val httpBindingResolver: HttpBindingResolver =
@ -74,11 +74,11 @@ class AwsQueryCompatible(
ProtocolFunctions.crossOperationFn("parse_http_error_metadata") { fnName ->
rustTemplate(
"""
pub fn $fnName(response: &#{Response}<#{Bytes}>) -> Result<#{ErrorMetadataBuilder}, #{JsonError}> {
pub fn $fnName(_response_status: u16, response_headers: &#{HeaderMap}, response_body: &[u8]) -> Result<#{ErrorMetadataBuilder}, #{JsonError}> {
let mut builder =
#{json_errors}::parse_error_metadata(response.body(), response.headers())?;
#{json_errors}::parse_error_metadata(response_body, response_headers)?;
if let Some((error_code, error_type)) =
#{aws_query_compatible_errors}::parse_aws_query_compatible_error(response.headers())
#{aws_query_compatible_errors}::parse_aws_query_compatible_error(response_headers)
{
builder = builder.code(error_code);
builder = builder.custom("type", error_type);

View File

@ -52,10 +52,10 @@ class Ec2QueryProtocol(private val codegenContext: CodegenContext) : Protocol {
override fun parseHttpErrorMetadata(operationShape: OperationShape): RuntimeType =
ProtocolFunctions.crossOperationFn("parse_http_error_metadata") { fnName ->
rustBlockTemplate(
"pub fn $fnName(response: &#{Response}<#{Bytes}>) -> Result<#{ErrorMetadataBuilder}, #{XmlDecodeError}>",
"pub fn $fnName(_response_status: u16, _response_headers: &#{HeaderMap}, response_body: &[u8]) -> Result<#{ErrorMetadataBuilder}, #{XmlDecodeError}>",
*errorScope,
) {
rust("#T::parse_error_metadata(response.body().as_ref())", ec2QueryErrors)
rust("#T::parse_error_metadata(response_body)", ec2QueryErrors)
}
}

View File

@ -46,7 +46,7 @@ interface Protocol {
/**
* Generates a function signature like the following:
* ```rust
* fn parse_http_error_metadata(response: &Response<Bytes>) -> aws_smithy_types::error::Builder
* fn parse_http_error_metadata(response_status: u16, response_headers: HeaderMap, response_body: &[u8]) -> aws_smithy_types::error::Builder
* ```
*/
fun parseHttpErrorMetadata(operationShape: OperationShape): RuntimeType

View File

@ -100,8 +100,8 @@ open class RestJson(val codegenContext: CodegenContext) : Protocol {
ProtocolFunctions.crossOperationFn("parse_http_error_metadata") { fnName ->
rustTemplate(
"""
pub fn $fnName(response: &#{Response}<#{Bytes}>) -> Result<#{ErrorMetadataBuilder}, #{JsonError}> {
#{json_errors}::parse_error_metadata(response.body(), response.headers())
pub fn $fnName(_response_status: u16, response_headers: &#{HeaderMap}, response_body: &[u8]) -> Result<#{ErrorMetadataBuilder}, #{JsonError}> {
#{json_errors}::parse_error_metadata(response_body, response_headers)
}
""",
*errorScope,

View File

@ -51,10 +51,10 @@ open class RestXml(val codegenContext: CodegenContext) : Protocol {
override fun parseHttpErrorMetadata(operationShape: OperationShape): RuntimeType =
ProtocolFunctions.crossOperationFn("parse_http_error_metadata") { fnName ->
rustBlockTemplate(
"pub fn $fnName(response: &#{Response}<#{Bytes}>) -> Result<#{ErrorMetadataBuilder}, #{XmlDecodeError}>",
"pub fn $fnName(_response_status: u16, _response_headers: &#{HeaderMap}, response_body: &[u8]) -> Result<#{ErrorMetadataBuilder}, #{XmlDecodeError}>",
*errorScope,
) {
rust("#T::parse_error_metadata(response.body().as_ref())", restXmlErrors)
rust("#T::parse_error_metadata(response_body)", restXmlErrors)
}
}

View File

@ -433,6 +433,21 @@ impl<E, R> SdkError<E, R> {
ServiceError(context) => Ok(context.source.into()),
}
}
/// Maps the service error type in `SdkError::ServiceError`
#[doc(hidden)]
pub fn map_service_error<E2>(self, map: impl FnOnce(E) -> E2) -> SdkError<E2, R> {
match self {
Self::ServiceError(context) => SdkError::<E2, R>::ServiceError(ServiceError {
source: map(context.source),
raw: context.raw,
}),
Self::ConstructionFailure(context) => SdkError::<E2, R>::ConstructionFailure(context),
Self::DispatchFailure(context) => SdkError::<E2, R>::DispatchFailure(context),
Self::ResponseError(context) => SdkError::<E2, R>::ResponseError(context),
Self::TimeoutError(context) => SdkError::<E2, R>::TimeoutError(context),
}
}
}
impl<E, R> Display for SdkError<E, R> {

View File

@ -14,6 +14,7 @@ publish = false
aws-smithy-types = { path = "../aws-smithy-types" }
aws-smithy-http = { path = "../aws-smithy-http" }
tokio = { version = "1.25", features = ["sync"] }
http = "0.2.3"
[package.metadata.docs.rs]
all-features = true

View File

@ -1,3 +1,7 @@
allowed_external_types = [
"aws_smithy_types::*",
"aws_smithy_http::*",
"http::request::Request",
"http::response::Response",
]

View File

@ -0,0 +1,22 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/// Smithy identity used by auth and signing.
pub mod identity;
/// Smithy interceptors for smithy clients.
///
/// Interceptors are lifecycle hooks that can read/modify requests and responses.
pub mod interceptors;
pub mod orchestrator;
/// Smithy code related to retry handling and token bucket.
///
/// This code defines when and how failed requests should be retried. It also defines the behavior
/// used to limit the rate that requests are sent.
pub mod retries;
/// Runtime plugin type definitions.
pub mod runtime_plugin;

View File

@ -0,0 +1,67 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
use aws_smithy_types::DateTime;
use std::any::Any;
use std::fmt::Debug;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct Identity {
data: Arc<dyn Any + Send + Sync>,
expiration: Option<DateTime>,
}
impl Identity {
pub fn new(data: impl Any + Send + Sync, expiration: Option<DateTime>) -> Self {
Self {
data: Arc::new(data),
expiration,
}
}
pub fn data<T: 'static>(&self) -> Option<&T> {
self.data.downcast_ref()
}
pub fn expiration(&self) -> Option<&DateTime> {
self.expiration.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use aws_smithy_types::date_time::Format;
#[test]
fn check_send_sync() {
fn is_send_sync<T: Send + Sync>(_: T) {}
is_send_sync(Identity::new("foo", None));
}
#[test]
fn create_retrieve_identity() {
#[derive(Debug)]
struct MyIdentityData {
first: String,
last: String,
}
let expiration =
DateTime::from_str("2023-03-15T00:00:00.000Z", Format::DateTimeWithOffset).unwrap();
let identity = Identity::new(
MyIdentityData {
first: "foo".into(),
last: "bar".into(),
},
Some(expiration.clone()),
);
assert_eq!("foo", identity.data::<MyIdentityData>().unwrap().first);
assert_eq!("bar", identity.data::<MyIdentityData>().unwrap().last);
assert_eq!(Some(&expiration), identity.expiration());
}
}

View File

@ -0,0 +1,622 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
pub mod context;
pub mod error;
use crate::config_bag::ConfigBag;
pub use context::InterceptorContext;
pub use error::InterceptorError;
macro_rules! interceptor_trait_fn {
($name:ident, $docs:tt) => {
#[doc = $docs]
fn $name(
&mut self,
context: &InterceptorContext<TxReq, TxRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
};
}
/// An interceptor allows injecting code into the SDK s request execution pipeline.
///
/// ## Terminology:
/// - An execution is one end-to-end invocation against an SDK client.
/// - An attempt is an attempt at performing an execution. By default executions are retried multiple
/// times based on the client s retry strategy.
/// - A hook is a single method on the interceptor, allowing injection of code into a specific part
/// of the SDK s request execution pipeline. Hooks are either "read" hooks, which make it possible
/// to read in-flight request or response messages, or "read/write" hooks, which make it possible
/// to modify in-flight request or output messages.
pub trait Interceptor<TxReq, TxRes> {
interceptor_trait_fn!(
read_before_execution,
"
A hook called at the start of an execution, before the SDK
does anything else.
**When:** This will **ALWAYS** be called once per execution. The duration
between invocation of this hook and `after_execution` is very close
to full duration of the execution.
**Available Information:** The [InterceptorContext::input()] is
**ALWAYS** available. Other information **WILL NOT** be available.
**Error Behavior:** Errors raised by this hook will be stored
until all interceptors have had their `before_execution` invoked.
Other hooks will then be skipped and execution will jump to
`modify_before_completion` with the raised error as the
[InterceptorContext::output_or_error()]. If multiple
`before_execution` methods raise errors, the latest
will be used and earlier ones will be logged and dropped.
"
);
interceptor_trait_fn!(
modify_before_serialization,
"
A hook called before the input message is marshalled into a
transport message.
This method has the ability to modify and return a new
request message of the same type.
**When:** This will **ALWAYS** be called once per execution, except when a
failure occurs earlier in the request pipeline.
**Available Information:** The [InterceptorContext::input()] is
**ALWAYS** available. This request may have been modified by earlier
`modify_before_serialization` hooks, and may be modified further by
later hooks. Other information **WILL NOT** be available.
**Error Behavior:** If errors are raised by this hook,
execution will jump to `modify_before_completion` with the raised
error as the [InterceptorContext::output_or_error()].
**Return Constraints:** The input message returned by this hook
MUST be the same type of input message passed into this hook.
If not, an error will immediately be raised.
"
);
interceptor_trait_fn!(
read_before_serialization,
"
A hook called before the input message is marshalled
into a transport
message.
**When:** This will **ALWAYS** be called once per execution, except when a
failure occurs earlier in the request pipeline. The
duration between invocation of this hook and `after_serialization` is
very close to the amount of time spent marshalling the request.
**Available Information:** The [InterceptorContext::input()] is
**ALWAYS** available. Other information **WILL NOT** be available.
**Error Behavior:** If errors are raised by this hook,
execution will jump to `modify_before_completion` with the raised
error as the [InterceptorContext::output_or_error()].
"
);
interceptor_trait_fn!(
read_after_serialization,
"
/// A hook called after the input message is marshalled into
/// a transport message.
///
/// **When:** This will **ALWAYS** be called once per execution, except when a
/// failure occurs earlier in the request pipeline. The duration
/// between invocation of this hook and `before_serialization` is very
/// close to the amount of time spent marshalling the request.
///
/// **Available Information:** The [InterceptorContext::input()]
/// and [InterceptorContext::request()] are **ALWAYS** available.
/// Other information **WILL NOT** be available.
///
/// **Error Behavior:** If errors are raised by this hook,
/// execution will jump to `modify_before_completion` with the raised
/// error as the [InterceptorContext::output_or_error()].
"
);
interceptor_trait_fn!(
modify_before_retry_loop,
"
A hook called before the retry loop is entered. This method
has the ability to modify and return a new transport request
message of the same type, except when a failure occurs earlier in the request pipeline.
**Available Information:** The [InterceptorContext::input()]
and [InterceptorContext::request()] are **ALWAYS** available.
Other information **WILL NOT** be available.
**Error Behavior:** If errors are raised by this hook,
execution will jump to `modify_before_completion` with the raised
error as the [InterceptorContext::output_or_error()].
**Return Constraints:** The transport request message returned by this
hook MUST be the same type of request message passed into this hook
If not, an error will immediately be raised.
"
);
interceptor_trait_fn!(
read_before_attempt,
"
A hook called before each attempt at sending the transmission
request message to the service.
**When:** This will **ALWAYS** be called once per attempt, except when a
failure occurs earlier in the request pipeline. This method will be
called multiple times in the event of retries.
**Available Information:** The [InterceptorContext::input()]
and [InterceptorContext::request()] are **ALWAYS** available.
Other information **WILL NOT** be available. In the event of retries,
the `InterceptorContext` will not include changes made in previous
attempts (e.g. by request signers or other interceptors).
**Error Behavior:** Errors raised by this hook will be stored
until all interceptors have had their `before_attempt` invoked.
Other hooks will then be skipped and execution will jump to
`modify_before_attempt_completion` with the raised error as the
[InterceptorContext::output_or_error()]. If multiple
`before_attempt` methods raise errors, the latest will be used
and earlier ones will be logged and dropped.
"
);
interceptor_trait_fn!(
modify_before_signing,
"
A hook called before the transport request message is signed.
This method has the ability to modify and return a new transport
request message of the same type.
**When:** This will **ALWAYS** be called once per attempt, except when a
failure occurs earlier in the request pipeline. This method may be
called multiple times in the event of retries.
**Available Information:** The [InterceptorContext::input()]
and [InterceptorContext::request()] are **ALWAYS** available.
The `http::Request` may have been modified by earlier
`modify_before_signing` hooks, and may be modified further by later
hooks. Other information **WILL NOT** be available. In the event of
retries, the `InterceptorContext` will not include changes made
in previous attempts
(e.g. by request signers or other interceptors).
**Error Behavior:** If errors are raised by this
hook, execution will jump to `modify_before_attempt_completion` with
the raised error as the [InterceptorContext::output_or_error()].
**Return Constraints:** The transport request message returned by this
hook MUST be the same type of request message passed into this hook
If not, an error will immediately be raised.
"
);
interceptor_trait_fn!(
read_before_signing,
"
A hook called before the transport request message is signed.
**When:** This will **ALWAYS** be called once per attempt, except when a
failure occurs earlier in the request pipeline. This method may be
called multiple times in the event of retries. The duration between
invocation of this hook and `after_signing` is very close to
the amount of time spent signing the request.
**Available Information:** The [InterceptorContext::input()]
and [InterceptorContext::request()] are **ALWAYS** available.
Other information **WILL NOT** be available. In the event of retries,
the `InterceptorContext` will not include changes made in previous
attempts (e.g. by request signers or other interceptors).
**Error Behavior:** If errors are raised by this
hook, execution will jump to `modify_before_attempt_completion` with
the raised error as the [InterceptorContext::output_or_error()].
"
);
interceptor_trait_fn!(
read_after_signing,
"
A hook called after the transport request message is signed.
**When:** This will **ALWAYS** be called once per attempt, except when a
failure occurs earlier in the request pipeline. This method may be
called multiple times in the event of retries. The duration between
invocation of this hook and `before_signing` is very close to
the amount of time spent signing the request.
**Available Information:** The [InterceptorContext::input()]
and [InterceptorContext::request()] are **ALWAYS** available.
Other information **WILL NOT** be available. In the event of retries,
the `InterceptorContext` will not include changes made in previous
attempts (e.g. by request signers or other interceptors).
**Error Behavior:** If errors are raised by this
hook, execution will jump to `modify_before_attempt_completion` with
the raised error as the [InterceptorContext::output_or_error()].
"
);
interceptor_trait_fn!(
modify_before_transmit,
"
/// A hook called before the transport request message is sent to the
/// service. This method has the ability to modify and return
/// a new transport request message of the same type.
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs earlier in the request pipeline. This method may be
/// called multiple times in the event of retries.
///
/// **Available Information:** The [InterceptorContext::input()]
/// and [InterceptorContext::request()] are **ALWAYS** available.
/// The `http::Request` may have been modified by earlier
/// `modify_before_transmit` hooks, and may be modified further by later
/// hooks. Other information **WILL NOT** be available.
/// In the event of retries, the `InterceptorContext` will not include
/// changes made in previous attempts (e.g. by request signers or
other interceptors).
**Error Behavior:** If errors are raised by this
hook, execution will jump to `modify_before_attempt_completion` with
the raised error as the [InterceptorContext::output_or_error()].
**Return Constraints:** The transport request message returned by this
hook MUST be the same type of request message passed into this hook
If not, an error will immediately be raised.
"
);
interceptor_trait_fn!(
read_before_transmit,
"
A hook called before the transport request message is sent to the
service.
**When:** This will **ALWAYS** be called once per attempt, except when a
failure occurs earlier in the request pipeline. This method may be
called multiple times in the event of retries. The duration between
invocation of this hook and `after_transmit` is very close to
the amount of time spent communicating with the service.
Depending on the protocol, the duration may not include the
time spent reading the response data.
**Available Information:** The [InterceptorContext::input()]
and [InterceptorContext::request()] are **ALWAYS** available.
Other information **WILL NOT** be available. In the event of retries,
the `InterceptorContext` will not include changes made in previous
attempts (e.g. by request signers or other interceptors).
**Error Behavior:** If errors are raised by this
hook, execution will jump to `modify_before_attempt_completion` with
the raised error as the [InterceptorContext::output_or_error()].
"
);
interceptor_trait_fn!(
read_after_transmit,
"
A hook called after the transport request message is sent to the
service and a transport response message is received.
**When:** This will **ALWAYS** be called once per attempt, except when a
failure occurs earlier in the request pipeline. This method may be
called multiple times in the event of retries. The duration between
invocation of this hook and `before_transmit` is very close to
the amount of time spent communicating with the service.
Depending on the protocol, the duration may not include the time
spent reading the response data.
**Available Information:** The [InterceptorContext::input()],
[InterceptorContext::request()] and
[InterceptorContext::response()] are **ALWAYS** available.
Other information **WILL NOT** be available. In the event of retries,
the `InterceptorContext` will not include changes made in previous
attempts (e.g. by request signers or other interceptors).
**Error Behavior:** If errors are raised by this
hook, execution will jump to `modify_before_attempt_completion` with
the raised error as the [InterceptorContext::output_or_error()].
"
);
interceptor_trait_fn!(
modify_before_deserialization,
"
A hook called before the transport response message is unmarshalled.
This method has the ability to modify and return a new transport
response message of the same type.
**When:** This will **ALWAYS** be called once per attempt, except when a
failure occurs earlier in the request pipeline. This method may be
called multiple times in the event of retries.
**Available Information:** The [InterceptorContext::input()],
[InterceptorContext::request()] and
[InterceptorContext::response()] are **ALWAYS** available.
The transmit_response may have been modified by earlier
`modify_before_deserialization` hooks, and may be modified further by
later hooks. Other information **WILL NOT** be available. In the event of
retries, the `InterceptorContext` will not include changes made in
previous attempts (e.g. by request signers or other interceptors).
**Error Behavior:** If errors are raised by this
hook, execution will jump to `modify_before_attempt_completion` with
the raised error as the
[InterceptorContext::output_or_error()].
**Return Constraints:** The transport response message returned by this
hook MUST be the same type of response message passed into
this hook. If not, an error will immediately be raised.
"
);
interceptor_trait_fn!(
read_before_deserialization,
"
A hook called before the transport response message is unmarshalled
**When:** This will **ALWAYS** be called once per attempt, except when a
failure occurs earlier in the request pipeline. This method may be
called multiple times in the event of retries. The duration between
invocation of this hook and `after_deserialization` is very close
to the amount of time spent unmarshalling the service response.
Depending on the protocol and operation, the duration may include
the time spent downloading the response data.
**Available Information:** The [InterceptorContext::input()],
[InterceptorContext::request()] and
[InterceptorContext::response()] are **ALWAYS** available.
Other information **WILL NOT** be available. In the event of retries,
the `InterceptorContext` will not include changes made in previous
attempts (e.g. by request signers or other interceptors).
**Error Behavior:** If errors are raised by this
hook, execution will jump to `modify_before_attempt_completion`
with the raised error as the [InterceptorContext::output_or_error()].
"
);
interceptor_trait_fn!(
read_after_deserialization,
"
A hook called after the transport response message is unmarshalled.
**When:** This will **ALWAYS** be called once per attempt, except when a
failure occurs earlier in the request pipeline. The duration
between invocation of this hook and `before_deserialization` is
very close to the amount of time spent unmarshalling the
service response. Depending on the protocol and operation,
the duration may include the time spent downloading
the response data.
**Available Information:** The [InterceptorContext::input()],
[InterceptorContext::request()],
[InterceptorContext::response()] and
[InterceptorContext::output_or_error()] are **ALWAYS** available. In the event
of retries, the `InterceptorContext` will not include changes made
in previous attempts (e.g. by request signers or other interceptors).
**Error Behavior:** If errors are raised by this
hook, execution will jump to `modify_before_attempt_completion` with
the raised error as the [InterceptorContext::output_or_error()].
"
);
interceptor_trait_fn!(
modify_before_attempt_completion,
"
A hook called when an attempt is completed. This method has the
ability to modify and return a new output message or error
matching the currently-executing operation.
**When:** This will **ALWAYS** be called once per attempt, except when a
failure occurs before `before_attempt`. This method may
be called multiple times in the event of retries.
**Available Information:** The [InterceptorContext::input()],
[InterceptorContext::request()],
[InterceptorContext::response()] and
[InterceptorContext::output_or_error()] are **ALWAYS** available. In the event
of retries, the `InterceptorContext` will not include changes made
in previous attempts (e.g. by request signers or other interceptors).
**Error Behavior:** If errors are raised by this
hook, execution will jump to `after_attempt` with
the raised error as the [InterceptorContext::output_or_error()].
**Return Constraints:** Any output message returned by this
hook MUST match the operation being invoked. Any error type can be
returned, replacing the response currently in the context.
"
);
interceptor_trait_fn!(
read_after_attempt,
"
A hook called when an attempt is completed.
**When:** This will **ALWAYS** be called once per attempt, as long as
`before_attempt` has been executed.
**Available Information:** The [InterceptorContext::input()],
[InterceptorContext::request()] and
[InterceptorContext::output_or_error()] are **ALWAYS** available.
The [InterceptorContext::response()] is available if a
response was received by the service for this attempt.
In the event of retries, the `InterceptorContext` will not include
changes made in previous attempts (e.g. by request signers or other
interceptors).
**Error Behavior:** Errors raised by this hook will be stored
until all interceptors have had their `after_attempt` invoked.
If multiple `after_execution` methods raise errors, the latest
will be used and earlier ones will be logged and dropped. If the
retry strategy determines that the execution is retryable,
execution will then jump to `before_attempt`. Otherwise,
execution will jump to `modify_before_attempt_completion` with the
raised error as the [InterceptorContext::output_or_error()].
"
);
interceptor_trait_fn!(
modify_before_completion,
"
A hook called when an execution is completed.
This method has the ability to modify and return a new
output message or error matching the currently - executing
operation.
**When:** This will **ALWAYS** be called once per execution.
**Available Information:** The [InterceptorContext::input()]
and [InterceptorContext::output_or_error()] are **ALWAYS** available. The
[InterceptorContext::request()]
and [InterceptorContext::response()] are available if the
execution proceeded far enough for them to be generated.
**Error Behavior:** If errors are raised by this
hook , execution will jump to `after_attempt` with
the raised error as the [InterceptorContext::output_or_error()].
**Return Constraints:** Any output message returned by this
hook MUST match the operation being invoked. Any error type can be
returned , replacing the response currently in the context.
"
);
interceptor_trait_fn!(
read_after_execution,
"
A hook called when an execution is completed.
**When:** This will **ALWAYS** be called once per execution. The duration
between invocation of this hook and `before_execution` is very
close to the full duration of the execution.
**Available Information:** The [InterceptorContext::input()]
and [InterceptorContext::output_or_error()] are **ALWAYS** available. The
[InterceptorContext::request()] and
[InterceptorContext::response()] are available if the
execution proceeded far enough for them to be generated.
**Error Behavior:** Errors raised by this hook will be stored
until all interceptors have had their `after_execution` invoked.
The error will then be treated as the
[InterceptorContext::output_or_error()] to the customer. If multiple
`after_execution` methods raise errors , the latest will be
used and earlier ones will be logged and dropped.
"
);
}
pub struct Interceptors<TxReq, TxRes> {
client_interceptors: Vec<Box<dyn Interceptor<TxReq, TxRes>>>,
operation_interceptors: Vec<Box<dyn Interceptor<TxReq, TxRes>>>,
}
impl<TxReq, TxRes> Default for Interceptors<TxReq, TxRes> {
fn default() -> Self {
Self {
client_interceptors: Vec::new(),
operation_interceptors: Vec::new(),
}
}
}
macro_rules! interceptor_impl_fn {
(context, $name:ident) => {
interceptor_impl_fn!(context, $name, $name);
};
(mut context, $name:ident) => {
interceptor_impl_fn!(mut context, $name, $name);
};
(context, $outer_name:ident, $inner_name:ident) => {
pub fn $outer_name(
&mut self,
context: &InterceptorContext<TxReq, TxRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.client_interceptors.iter_mut() {
interceptor.$inner_name(context, cfg)?;
}
Ok(())
}
};
(mut context, $outer_name:ident, $inner_name:ident) => {
pub fn $outer_name(
&mut self,
context: &mut InterceptorContext<TxReq, TxRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.client_interceptors.iter_mut() {
interceptor.$inner_name(context, cfg)?;
}
Ok(())
}
};
}
impl<TxReq, TxRes> Interceptors<TxReq, TxRes> {
pub fn new() -> Self {
Self::default()
}
pub fn with_client_interceptor(
&mut self,
interceptor: impl Interceptor<TxReq, TxRes> + 'static,
) -> &mut Self {
self.client_interceptors.push(Box::new(interceptor));
self
}
pub fn with_operation_interceptor(
&mut self,
interceptor: impl Interceptor<TxReq, TxRes> + 'static,
) -> &mut Self {
self.operation_interceptors.push(Box::new(interceptor));
self
}
interceptor_impl_fn!(context, client_read_before_execution, read_before_execution);
interceptor_impl_fn!(
context,
operation_read_before_execution,
read_before_execution
);
interceptor_impl_fn!(mut context, modify_before_serialization);
interceptor_impl_fn!(context, read_before_serialization);
interceptor_impl_fn!(context, read_after_serialization);
interceptor_impl_fn!(mut context, modify_before_retry_loop);
interceptor_impl_fn!(context, read_before_attempt);
interceptor_impl_fn!(mut context, modify_before_signing);
interceptor_impl_fn!(context, read_before_signing);
interceptor_impl_fn!(context, read_after_signing);
interceptor_impl_fn!(mut context, modify_before_transmit);
interceptor_impl_fn!(context, read_before_transmit);
interceptor_impl_fn!(context, read_after_transmit);
interceptor_impl_fn!(mut context, modify_before_deserialization);
interceptor_impl_fn!(context, read_before_deserialization);
interceptor_impl_fn!(context, read_after_deserialization);
interceptor_impl_fn!(mut context, modify_before_attempt_completion);
interceptor_impl_fn!(context, read_after_attempt);
interceptor_impl_fn!(mut context, modify_before_completion);
interceptor_impl_fn!(context, read_after_execution);
}

View File

@ -0,0 +1,140 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
use super::InterceptorError;
use crate::type_erasure::TypeErasedBox;
pub type Input = TypeErasedBox;
pub type Output = TypeErasedBox;
pub type Error = TypeErasedBox;
pub type OutputOrError = Result<Output, Error>;
/// A container for the data currently available to an interceptor.
pub struct InterceptorContext<Request, Response> {
input: Input,
output_or_error: Option<OutputOrError>,
request: Option<Request>,
response: Option<Response>,
}
// TODO(interceptors) we could use types to ensure that people calling methods on interceptor context can't access
// field that haven't been set yet.
impl<Request, Response> InterceptorContext<Request, Response> {
pub fn new(input: Input) -> Self {
Self {
input,
output_or_error: None,
request: None,
response: None,
}
}
/// Retrieve the modeled request for the operation being invoked.
pub fn input(&self) -> &Input {
&self.input
}
/// Retrieve the modeled request for the operation being invoked.
pub fn input_mut(&mut self) -> &mut Input {
&mut self.input
}
/// Retrieve the transmittable request for the operation being invoked.
/// This will only be available once request marshalling has completed.
pub fn request(&self) -> Result<&Request, InterceptorError> {
self.request
.as_ref()
.ok_or_else(InterceptorError::invalid_request_access)
}
/// Retrieve the transmittable request for the operation being invoked.
/// This will only be available once request marshalling has completed.
pub fn request_mut(&mut self) -> Result<&mut Request, InterceptorError> {
self.request
.as_mut()
.ok_or_else(InterceptorError::invalid_request_access)
}
/// Retrieve the response to the transmittable response for the operation
/// being invoked. This will only be available once transmission has
/// completed.
pub fn response(&self) -> Result<&Response, InterceptorError> {
self.response
.as_ref()
.ok_or_else(InterceptorError::invalid_response_access)
}
/// Retrieve the response to the transmittable response for the operation
/// being invoked. This will only be available once transmission has
/// completed.
pub fn response_mut(&mut self) -> Result<&mut Response, InterceptorError> {
self.response
.as_mut()
.ok_or_else(InterceptorError::invalid_response_access)
}
/// Retrieve the response to the customer. This will only be available
/// once the `response` has been unmarshalled or the attempt/execution has failed.
pub fn output_or_error(&self) -> Result<Result<&Output, &Error>, InterceptorError> {
self.output_or_error
.as_ref()
.ok_or_else(InterceptorError::invalid_modeled_response_access)
.map(|res| res.as_ref())
}
/// Retrieve the response to the customer. This will only be available
/// once the `response` has been unmarshalled or the
/// attempt/execution has failed.
pub fn output_or_error_mut(&mut self) -> Result<&mut Result<Output, Error>, InterceptorError> {
self.output_or_error
.as_mut()
.ok_or_else(InterceptorError::invalid_modeled_response_access)
}
// There is no set_modeled_request method because that can only be set once, during context construction
pub fn set_request(&mut self, request: Request) {
if self.request.is_some() {
panic!("Called set_request but a request was already set. This is a bug. Please report it.");
}
self.request = Some(request);
}
pub fn set_response(&mut self, response: Response) {
if self.response.is_some() {
panic!("Called set_response but a transmit_response was already set. This is a bug. Please report it.");
}
self.response = Some(response);
}
pub fn set_output_or_error(&mut self, output: Result<Output, Error>) {
if self.output_or_error.is_some() {
panic!(
"Called set_output but an output was already set. This is a bug. Please report it."
);
}
self.output_or_error = Some(output);
}
#[doc(hidden)]
pub fn into_parts(
self,
) -> (
Input,
Option<OutputOrError>,
Option<Request>,
Option<Response>,
) {
(
self.input,
self.output_or_error,
self.request,
self.response,
)
}
}

View File

@ -190,16 +190,16 @@ impl InterceptorError {
}
}
/// Create a new error indicating that an interceptor tried to access the tx_request out of turn
pub fn invalid_tx_request_access() -> Self {
pub fn invalid_request_access() -> Self {
Self {
kind: ErrorKind::InvalidTxRequestAccess,
kind: ErrorKind::InvalidRequestAccess,
source: None,
}
}
/// Create a new error indicating that an interceptor tried to access the tx_response out of turn
pub fn invalid_tx_response_access() -> Self {
pub fn invalid_response_access() -> Self {
Self {
kind: ErrorKind::InvalidTxResponseAccess,
kind: ErrorKind::InvalidResponseAccess,
source: None,
}
}
@ -253,10 +253,10 @@ enum ErrorKind {
/// An error occurred within the read_after_execution interceptor
ReadAfterExecution,
// There is no InvalidModeledRequestAccess because it's always accessible
/// An interceptor tried to access the tx_request out of turn
InvalidTxRequestAccess,
/// An interceptor tried to access the tx_response out of turn
InvalidTxResponseAccess,
/// An interceptor tried to access the request out of turn
InvalidRequestAccess,
/// An interceptor tried to access the response out of turn
InvalidResponseAccess,
/// An interceptor tried to access the modeled_response out of turn
InvalidModeledResponseAccess,
}
@ -321,13 +321,12 @@ impl fmt::Display for InterceptorError {
ReadAfterExecution => {
write!(f, "read_after_execution interceptor encountered an error")
}
InvalidTxRequestAccess => {
write!(f, "tried to access tx_request before request serialization")
InvalidRequestAccess => {
write!(f, "tried to access request before request serialization")
}
InvalidResponseAccess => {
write!(f, "tried to access response before transmitting a request")
}
InvalidTxResponseAccess => write!(
f,
"tried to access tx_response before transmitting a request"
),
InvalidModeledResponseAccess => write!(
f,
"tried to access modeled_response before response deserialization"

View File

@ -0,0 +1,374 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
use crate::client::identity::Identity;
use crate::client::interceptors::context::{Input, OutputOrError};
use crate::client::interceptors::InterceptorContext;
use crate::config_bag::ConfigBag;
use crate::type_erasure::{TypeErasedBox, TypedBox};
use aws_smithy_http::body::SdkBody;
use aws_smithy_http::property_bag::PropertyBag;
use std::any::Any;
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
pub type HttpRequest = http::Request<SdkBody>;
pub type HttpResponse = http::Response<SdkBody>;
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub type BoxFallibleFut<T> = Pin<Box<dyn Future<Output = Result<T, BoxError>>>>;
pub trait TraceProbe: Send + Sync + Debug {
fn dispatch_events(&self, cfg: &ConfigBag) -> BoxFallibleFut<()>;
}
pub trait RequestSerializer: Send + Sync + Debug {
fn serialize_input(&self, input: &Input, cfg: &ConfigBag) -> Result<HttpRequest, BoxError>;
}
pub trait ResponseDeserializer: Send + Sync + Debug {
fn deserialize_streaming(&self, response: &mut HttpResponse) -> Option<OutputOrError> {
let _ = response;
None
}
fn deserialize_nonstreaming(&self, response: &HttpResponse) -> OutputOrError;
}
pub trait Connection: Send + Sync + Debug {
fn call(&self, request: &mut HttpRequest, cfg: &ConfigBag) -> BoxFallibleFut<HttpResponse>;
}
pub trait RetryStrategy: Send + Sync + Debug {
fn should_attempt_initial_request(&self, cfg: &ConfigBag) -> Result<(), BoxError>;
fn should_attempt_retry(
&self,
context: &InterceptorContext<HttpRequest, HttpResponse>,
cfg: &ConfigBag,
) -> Result<bool, BoxError>;
}
#[derive(Debug)]
pub struct AuthOptionResolverParams(TypeErasedBox);
impl AuthOptionResolverParams {
pub fn new<T: Any + Send + Sync + 'static>(params: T) -> Self {
Self(TypedBox::new(params).erase())
}
pub fn get<T: 'static>(&self) -> Option<&T> {
self.0.downcast_ref()
}
}
pub trait AuthOptionResolver: Send + Sync + Debug {
fn resolve_auth_options(
&self,
params: &AuthOptionResolverParams,
) -> Result<Vec<HttpAuthOption>, BoxError>;
}
#[derive(Clone, Debug)]
pub struct HttpAuthOption {
scheme_id: &'static str,
properties: Arc<PropertyBag>,
}
impl HttpAuthOption {
pub fn new(scheme_id: &'static str, properties: Arc<PropertyBag>) -> Self {
Self {
scheme_id,
properties,
}
}
pub fn scheme_id(&self) -> &'static str {
self.scheme_id
}
pub fn properties(&self) -> &PropertyBag {
&self.properties
}
}
pub trait IdentityResolver: Send + Sync + Debug {
fn resolve_identity(&self, cfg: &ConfigBag) -> Result<Identity, BoxError>;
}
#[derive(Debug)]
pub struct IdentityResolvers {
identity_resolvers: Vec<(&'static str, Box<dyn IdentityResolver>)>,
}
impl IdentityResolvers {
pub fn builder() -> builders::IdentityResolversBuilder {
builders::IdentityResolversBuilder::new()
}
pub fn identity_resolver(&self, identity_type: &'static str) -> Option<&dyn IdentityResolver> {
self.identity_resolvers
.iter()
.find(|resolver| resolver.0 == identity_type)
.map(|resolver| &*resolver.1)
}
}
#[derive(Debug)]
struct HttpAuthSchemesInner {
schemes: Vec<(&'static str, Box<dyn HttpAuthScheme>)>,
}
#[derive(Debug)]
pub struct HttpAuthSchemes {
inner: Arc<HttpAuthSchemesInner>,
}
impl HttpAuthSchemes {
pub fn builder() -> builders::HttpAuthSchemesBuilder {
Default::default()
}
pub fn scheme(&self, name: &'static str) -> Option<&dyn HttpAuthScheme> {
self.inner
.schemes
.iter()
.find(|scheme| scheme.0 == name)
.map(|scheme| &*scheme.1)
}
}
pub trait HttpAuthScheme: Send + Sync + Debug {
fn scheme_id(&self) -> &'static str;
fn identity_resolver(&self, identity_resolvers: &IdentityResolvers) -> &dyn IdentityResolver;
fn request_signer(&self) -> &dyn HttpRequestSigner;
}
pub trait HttpRequestSigner: Send + Sync + Debug {
/// Return a signed version of the given request using the given identity.
///
/// If the provided identity is incompatible with this signer, an error must be returned.
fn sign_request(
&self,
request: &HttpRequest,
identity: &Identity,
cfg: &ConfigBag,
) -> Result<HttpRequest, BoxError>;
}
pub trait EndpointResolver: Send + Sync + Debug {
fn resolve_and_apply_endpoint(
&self,
request: &mut HttpRequest,
cfg: &ConfigBag,
) -> Result<(), BoxError>;
}
pub trait ConfigBagAccessors {
fn auth_option_resolver_params(&self) -> &AuthOptionResolverParams;
fn set_auth_option_resolver_params(
&mut self,
auth_option_resolver_params: AuthOptionResolverParams,
);
fn auth_option_resolver(&self) -> &dyn AuthOptionResolver;
fn set_auth_option_resolver(&mut self, auth_option_resolver: impl AuthOptionResolver + 'static);
fn endpoint_resolver(&self) -> &dyn EndpointResolver;
fn set_endpoint_resolver(&mut self, endpoint_resolver: impl EndpointResolver + 'static);
fn identity_resolvers(&self) -> &IdentityResolvers;
fn set_identity_resolvers(&mut self, identity_resolvers: IdentityResolvers);
fn connection(&self) -> &dyn Connection;
fn set_connection(&mut self, connection: impl Connection + 'static);
fn http_auth_schemes(&self) -> &HttpAuthSchemes;
fn set_http_auth_schemes(&mut self, http_auth_schemes: HttpAuthSchemes);
fn request_serializer(&self) -> &dyn RequestSerializer;
fn set_request_serializer(&mut self, request_serializer: impl RequestSerializer + 'static);
fn response_deserializer(&self) -> &dyn ResponseDeserializer;
fn set_response_deserializer(
&mut self,
response_serializer: impl ResponseDeserializer + 'static,
);
fn retry_strategy(&self) -> &dyn RetryStrategy;
fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static);
fn trace_probe(&self) -> &dyn TraceProbe;
fn set_trace_probe(&mut self, trace_probe: impl TraceProbe + 'static);
}
impl ConfigBagAccessors for ConfigBag {
fn auth_option_resolver_params(&self) -> &AuthOptionResolverParams {
self.get::<AuthOptionResolverParams>()
.expect("auth option resolver params must be set")
}
fn set_auth_option_resolver_params(
&mut self,
auth_option_resolver_params: AuthOptionResolverParams,
) {
self.put::<AuthOptionResolverParams>(auth_option_resolver_params);
}
fn auth_option_resolver(&self) -> &dyn AuthOptionResolver {
&**self
.get::<Box<dyn AuthOptionResolver>>()
.expect("an auth option resolver must be set")
}
fn set_auth_option_resolver(
&mut self,
auth_option_resolver: impl AuthOptionResolver + 'static,
) {
self.put::<Box<dyn AuthOptionResolver>>(Box::new(auth_option_resolver));
}
fn http_auth_schemes(&self) -> &HttpAuthSchemes {
self.get::<HttpAuthSchemes>()
.expect("auth schemes must be set")
}
fn set_http_auth_schemes(&mut self, http_auth_schemes: HttpAuthSchemes) {
self.put::<HttpAuthSchemes>(http_auth_schemes);
}
fn retry_strategy(&self) -> &dyn RetryStrategy {
&**self
.get::<Box<dyn RetryStrategy>>()
.expect("a retry strategy must be set")
}
fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static) {
self.put::<Box<dyn RetryStrategy>>(Box::new(retry_strategy));
}
fn endpoint_resolver(&self) -> &dyn EndpointResolver {
&**self
.get::<Box<dyn EndpointResolver>>()
.expect("an endpoint resolver must be set")
}
fn set_endpoint_resolver(&mut self, endpoint_resolver: impl EndpointResolver + 'static) {
self.put::<Box<dyn EndpointResolver>>(Box::new(endpoint_resolver));
}
fn identity_resolvers(&self) -> &IdentityResolvers {
self.get::<IdentityResolvers>()
.expect("identity resolvers must be configured")
}
fn set_identity_resolvers(&mut self, identity_resolvers: IdentityResolvers) {
self.put::<IdentityResolvers>(identity_resolvers);
}
fn connection(&self) -> &dyn Connection {
&**self
.get::<Box<dyn Connection>>()
.expect("missing connector")
}
fn set_connection(&mut self, connection: impl Connection + 'static) {
self.put::<Box<dyn Connection>>(Box::new(connection));
}
fn request_serializer(&self) -> &dyn RequestSerializer {
&**self
.get::<Box<dyn RequestSerializer>>()
.expect("missing request serializer")
}
fn set_request_serializer(&mut self, request_serializer: impl RequestSerializer + 'static) {
self.put::<Box<dyn RequestSerializer>>(Box::new(request_serializer));
}
fn response_deserializer(&self) -> &dyn ResponseDeserializer {
&**self
.get::<Box<dyn ResponseDeserializer>>()
.expect("missing response deserializer")
}
fn set_response_deserializer(
&mut self,
response_deserializer: impl ResponseDeserializer + 'static,
) {
self.put::<Box<dyn ResponseDeserializer>>(Box::new(response_deserializer));
}
fn trace_probe(&self) -> &dyn TraceProbe {
&**self
.get::<Box<dyn TraceProbe>>()
.expect("missing trace probe")
}
fn set_trace_probe(&mut self, trace_probe: impl TraceProbe + 'static) {
self.put::<Box<dyn TraceProbe>>(Box::new(trace_probe));
}
}
pub mod builders {
use super::*;
#[derive(Debug, Default)]
pub struct IdentityResolversBuilder {
identity_resolvers: Vec<(&'static str, Box<dyn IdentityResolver>)>,
}
impl IdentityResolversBuilder {
pub fn new() -> Self {
Default::default()
}
pub fn identity_resolver(
mut self,
name: &'static str,
resolver: impl IdentityResolver + 'static,
) -> Self {
self.identity_resolvers
.push((name, Box::new(resolver) as _));
self
}
pub fn build(self) -> IdentityResolvers {
IdentityResolvers {
identity_resolvers: self.identity_resolvers,
}
}
}
#[derive(Debug, Default)]
pub struct HttpAuthSchemesBuilder {
schemes: Vec<(&'static str, Box<dyn HttpAuthScheme>)>,
}
impl HttpAuthSchemesBuilder {
pub fn new() -> Self {
Default::default()
}
pub fn auth_scheme(
mut self,
name: &'static str,
auth_scheme: impl HttpAuthScheme + 'static,
) -> Self {
self.schemes.push((name, Box::new(auth_scheme) as _));
self
}
pub fn build(self) -> HttpAuthSchemes {
HttpAuthSchemes {
inner: Arc::new(HttpAuthSchemesInner {
schemes: self.schemes,
}),
}
}
}
}

View File

@ -55,11 +55,11 @@ impl Token for Standard {
#[cfg(test)]
mod tests {
use super::Standard as Token;
use crate::retries::rate_limiting::token_bucket::Standard as TokenBucket;
use crate::client::retries::rate_limiting::token_bucket::Standard as TokenBucket;
#[test]
fn token_bucket_trait_is_dyn_safe() {
let _tb: Box<dyn crate::retries::rate_limiting::TokenBucket<Token = Token>> =
let _tb: Box<dyn crate::client::retries::rate_limiting::TokenBucket<Token = Token>> =
Box::new(TokenBucket::builder().build());
}
}

View File

@ -1,861 +0,0 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
pub mod context;
pub mod error;
use crate::config_bag::ConfigBag;
pub use context::InterceptorContext;
pub use error::InterceptorError;
/// An interceptor allows injecting code into the SDK s request execution pipeline.
///
/// ## Terminology:
/// - An execution is one end-to-end invocation against an SDK client.
/// - An attempt is an attempt at performing an execution. By default executions are retried multiple
/// times based on the client s retry strategy.
/// - A hook is a single method on the interceptor, allowing injection of code into a specific part
/// of the SDK s request execution pipeline. Hooks are either "read" hooks, which make it possible
/// to read in-flight request or response messages, or "read/write" hooks, which make it possible
/// to modify in-flight request or output messages.
pub trait Interceptor<ModReq, TxReq, TxRes, ModRes> {
/// A hook called at the start of an execution, before the SDK
/// does anything else.
///
/// **When:** This will **ALWAYS** be called once per execution. The duration
/// between invocation of this hook and `after_execution` is very close
/// to full duration of the execution.
///
/// **Available Information:** The [InterceptorContext::modeled_request()] is
/// **ALWAYS** available. Other information **WILL NOT** be available.
///
/// **Error Behavior:** Errors raised by this hook will be stored
/// until all interceptors have had their `before_execution` invoked.
/// Other hooks will then be skipped and execution will jump to
/// `modify_before_completion` with the raised error as the
/// [InterceptorContext::modeled_response()]. If multiple
/// `before_execution` methods raise errors, the latest
/// will be used and earlier ones will be logged and dropped.
fn read_before_execution(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called before the input message is marshalled into a
/// transport message.
/// This method has the ability to modify and return a new
/// request message of the same type.
///
/// **When:** This will **ALWAYS** be called once per execution, except when a
/// failure occurs earlier in the request pipeline.
///
/// **Available Information:** The [InterceptorContext::modeled_request()] is
/// **ALWAYS** available. This request may have been modified by earlier
/// `modify_before_serialization` hooks, and may be modified further by
/// later hooks. Other information **WILL NOT** be available.
///
/// **Error Behavior:** If errors are raised by this hook,
///
/// execution will jump to `modify_before_completion` with the raised
/// error as the [InterceptorContext::modeled_response()].
///
/// **Return Constraints:** The input message returned by this hook
/// MUST be the same type of input message passed into this hook.
/// If not, an error will immediately be raised.
fn modify_before_serialization(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called before the input message is marshalled
/// into a transport
/// message.
///
/// **When:** This will **ALWAYS** be called once per execution, except when a
/// failure occurs earlier in the request pipeline. The
/// duration between invocation of this hook and `after_serialization` is
/// very close to the amount of time spent marshalling the request.
///
/// **Available Information:** The [InterceptorContext::modeled_request()] is
/// **ALWAYS** available. Other information **WILL NOT** be available.
///
/// **Error Behavior:** If errors are raised by this hook,
/// execution will jump to `modify_before_completion` with the raised
/// error as the [InterceptorContext::modeled_response()].
fn read_before_serialization(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called after the input message is marshalled into
/// a transport message.
///
/// **When:** This will **ALWAYS** be called once per execution, except when a
/// failure occurs earlier in the request pipeline. The duration
/// between invocation of this hook and `before_serialization` is very
/// close to the amount of time spent marshalling the request.
///
/// **Available Information:** The [InterceptorContext::modeled_request()]
/// and [InterceptorContext::tx_request()] are **ALWAYS** available.
/// Other information **WILL NOT** be available.
///
/// **Error Behavior:** If errors are raised by this hook,
/// execution will jump to `modify_before_completion` with the raised
/// error as the [InterceptorContext::modeled_response()].
fn read_after_serialization(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called before the retry loop is entered. This method
/// has the ability to modify and return a new transport request
/// message of the same type, except when a failure occurs earlier in the request pipeline.
///
/// **Available Information:** The [InterceptorContext::modeled_request()]
/// and [InterceptorContext::tx_request()] are **ALWAYS** available.
/// Other information **WILL NOT** be available.
///
/// **Error Behavior:** If errors are raised by this hook,
/// execution will jump to `modify_before_completion` with the raised
/// error as the [InterceptorContext::modeled_response()].
///
/// **Return Constraints:** The transport request message returned by this
/// hook MUST be the same type of request message passed into this hook
/// If not, an error will immediately be raised.
fn modify_before_retry_loop(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called before each attempt at sending the transmission
/// request message to the service.
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs earlier in the request pipeline. This method will be
/// called multiple times in the event of retries.
///
/// **Available Information:** The [InterceptorContext::modeled_request()]
/// and [InterceptorContext::tx_request()] are **ALWAYS** available.
/// Other information **WILL NOT** be available. In the event of retries,
/// the `InterceptorContext` will not include changes made in previous
/// attempts (e.g. by request signers or other interceptors).
///
/// **Error Behavior:** Errors raised by this hook will be stored
/// until all interceptors have had their `before_attempt` invoked.
/// Other hooks will then be skipped and execution will jump to
/// `modify_before_attempt_completion` with the raised error as the
/// [InterceptorContext::modeled_response()]. If multiple
/// `before_attempt` methods raise errors, the latest will be used
/// and earlier ones will be logged and dropped.
fn read_before_attempt(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called before the transport request message is signed.
/// This method has the ability to modify and return a new transport
/// request message of the same type.
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs earlier in the request pipeline. This method may be
/// called multiple times in the event of retries.
///
/// **Available Information:** The [InterceptorContext::modeled_request()]
/// and [InterceptorContext::tx_request()] are **ALWAYS** available.
/// The `http::Request` may have been modified by earlier
/// `modify_before_signing` hooks, and may be modified further by later
/// hooks. Other information **WILL NOT** be available. In the event of
/// retries, the `InterceptorContext` will not include changes made
/// in previous attempts
/// (e.g. by request signers or other interceptors).
///
/// **Error Behavior:** If errors are raised by this
/// hook, execution will jump to `modify_before_attempt_completion` with
/// the raised error as the [InterceptorContext::modeled_response()].
///
/// **Return Constraints:** The transport request message returned by this
/// hook MUST be the same type of request message passed into this hook
///
/// If not, an error will immediately be raised.
fn modify_before_signing(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called before the transport request message is signed.
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs earlier in the request pipeline. This method may be
/// called multiple times in the event of retries. The duration between
/// invocation of this hook and `after_signing` is very close to
/// the amount of time spent signing the request.
///
/// **Available Information:** The [InterceptorContext::modeled_request()]
/// and [InterceptorContext::tx_request()] are **ALWAYS** available.
/// Other information **WILL NOT** be available. In the event of retries,
/// the `InterceptorContext` will not include changes made in previous
/// attempts (e.g. by request signers or other interceptors).
///
/// **Error Behavior:** If errors are raised by this
/// hook, execution will jump to `modify_before_attempt_completion` with
/// the raised error as the [InterceptorContext::modeled_response()].
fn read_before_signing(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called after the transport request message is signed.
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs earlier in the request pipeline. This method may be
/// called multiple times in the event of retries. The duration between
/// invocation of this hook and `before_signing` is very close to
/// the amount of time spent signing the request.
///
/// **Available Information:** The [InterceptorContext::modeled_request()]
/// and [InterceptorContext::tx_request()] are **ALWAYS** available.
/// Other information **WILL NOT** be available. In the event of retries,
/// the `InterceptorContext` will not include changes made in previous
/// attempts (e.g. by request signers or other interceptors).
///
/// **Error Behavior:** If errors are raised by this
/// hook, execution will jump to `modify_before_attempt_completion` with
/// the raised error as the [InterceptorContext::modeled_response()].
fn read_after_signing(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called before the transport request message is sent to the
/// service. This method has the ability to modify and return
/// a new transport request message of the same type.
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs earlier in the request pipeline. This method may be
/// called multiple times in the event of retries.
///
/// **Available Information:** The [InterceptorContext::modeled_request()]
/// and [InterceptorContext::tx_request()] are **ALWAYS** available.
/// The `http::Request` may have been modified by earlier
/// `modify_before_transmit` hooks, and may be modified further by later
/// hooks. Other information **WILL NOT** be available.
/// In the event of retries, the `InterceptorContext` will not include
/// changes made in previous attempts (e.g. by request signers or
/// other interceptors).
///
/// **Error Behavior:** If errors are raised by this
/// hook, execution will jump to `modify_before_attempt_completion` with
/// the raised error as the [InterceptorContext::modeled_response()].
///
/// **Return Constraints:** The transport request message returned by this
/// hook MUST be the same type of request message passed into this hook
///
/// If not, an error will immediately be raised.
fn modify_before_transmit(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called before the transport request message is sent to the
/// service.
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs earlier in the request pipeline. This method may be
/// called multiple times in the event of retries. The duration between
/// invocation of this hook and `after_transmit` is very close to
/// the amount of time spent communicating with the service.
/// Depending on the protocol, the duration may not include the
/// time spent reading the response data.
///
/// **Available Information:** The [InterceptorContext::modeled_request()]
/// and [InterceptorContext::tx_request()] are **ALWAYS** available.
/// Other information **WILL NOT** be available. In the event of retries,
/// the `InterceptorContext` will not include changes made in previous
/// attempts (e.g. by request signers or other interceptors).
///
///
/// **Error Behavior:** If errors are raised by this
/// hook, execution will jump to `modify_before_attempt_completion` with
/// the raised error as the [InterceptorContext::modeled_response()].
fn read_before_transmit(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called after the transport request message is sent to the
/// service and a transport response message is received.
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs earlier in the request pipeline. This method may be
/// called multiple times in the event of retries. The duration between
/// invocation of this hook and `before_transmit` is very close to
/// the amount of time spent communicating with the service.
/// Depending on the protocol, the duration may not include the time
/// spent reading the response data.
///
/// **Available Information:** The [InterceptorContext::modeled_request()],
/// [InterceptorContext::tx_request()] and
/// [InterceptorContext::tx_response()] are **ALWAYS** available.
/// Other information **WILL NOT** be available. In the event of retries,
/// the `InterceptorContext` will not include changes made in previous
/// attempts (e.g. by request signers or other interceptors).
///
/// **Error Behavior:** If errors are raised by this
/// hook, execution will jump to `modify_before_attempt_completion` with
/// the raised error as the [InterceptorContext::modeled_response()].
fn read_after_transmit(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called before the transport response message is unmarshalled.
/// This method has the ability to modify and return a new transport
/// response message of the same type.
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs earlier in the request pipeline. This method may be
/// called multiple times in the event of retries.
///
/// **Available Information:** The [InterceptorContext::modeled_request()],
/// [InterceptorContext::tx_request()] and
/// [InterceptorContext::tx_response()] are **ALWAYS** available.
/// The transmit_response may have been modified by earlier
/// `modify_before_deserialization` hooks, and may be modified further by
/// later hooks. Other information **WILL NOT** be available. In the event of
/// retries, the `InterceptorContext` will not include changes made in
/// previous attempts (e.g. by request signers or other interceptors).
///
/// **Error Behavior:** If errors are raised by this
/// hook, execution will jump to `modify_before_attempt_completion` with
/// the raised error as the
/// [InterceptorContext::modeled_response()].
///
/// **Return Constraints:** The transport response message returned by this
/// hook MUST be the same type of response message passed into
/// this hook. If not, an error will immediately be raised.
fn modify_before_deserialization(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called before the transport response message is unmarshalled
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs earlier in the request pipeline. This method may be
/// called multiple times in the event of retries. The duration between
/// invocation of this hook and `after_deserialization` is very close
/// to the amount of time spent unmarshalling the service response.
/// Depending on the protocol and operation, the duration may include
/// the time spent downloading the response data.
///
/// **Available Information:** The [InterceptorContext::modeled_request()],
/// [InterceptorContext::tx_request()] and
/// [InterceptorContext::tx_response()] are **ALWAYS** available.
/// Other information **WILL NOT** be available. In the event of retries,
/// the `InterceptorContext` will not include changes made in previous
/// attempts (e.g. by request signers or other interceptors).
///
/// **Error Behavior:** If errors are raised by this
/// hook, execution will jump to `modify_before_attempt_completion`
/// with the raised error as the [InterceptorContext::modeled_response()].
fn read_before_deserialization(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called after the transport response message is unmarshalled.
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs earlier in the request pipeline. The duration
/// between invocation of this hook and `before_deserialization` is
/// very close to the amount of time spent unmarshalling the
/// service response. Depending on the protocol and operation,
/// the duration may include the time spent downloading
/// the response data.
///
/// **Available Information:** The [InterceptorContext::modeled_request()],
/// [InterceptorContext::tx_request()],
/// [InterceptorContext::tx_response()] and
/// [InterceptorContext::modeled_response()] are **ALWAYS** available. In the event
/// of retries, the `InterceptorContext` will not include changes made
/// in previous attempts (e.g. by request signers or other interceptors).
///
/// **Error Behavior:** If errors are raised by this
/// hook, execution will jump to `modify_before_attempt_completion` with
/// the raised error as the [InterceptorContext::modeled_response()].
fn read_after_deserialization(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called when an attempt is completed. This method has the
/// ability to modify and return a new output message or error
/// matching the currently-executing operation.
///
/// **When:** This will **ALWAYS** be called once per attempt, except when a
/// failure occurs before `before_attempt`. This method may
/// be called multiple times in the event of retries.
///
/// **Available Information:** The [InterceptorContext::modeled_request()],
/// [InterceptorContext::tx_request()],
/// [InterceptorContext::tx_response()] and
/// [InterceptorContext::modeled_response()] are **ALWAYS** available. In the event
/// of retries, the `InterceptorContext` will not include changes made
/// in previous attempts (e.g. by request signers or other interceptors).
///
/// **Error Behavior:** If errors are raised by this
/// hook, execution will jump to `after_attempt` with
/// the raised error as the [InterceptorContext::modeled_response()].
///
/// **Return Constraints:** Any output message returned by this
/// hook MUST match the operation being invoked. Any error type can be
/// returned, replacing the response currently in the context.
fn modify_before_attempt_completion(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called when an attempt is completed.
///
/// **When:** This will **ALWAYS** be called once per attempt, as long as
/// `before_attempt` has been executed.
///
/// **Available Information:** The [InterceptorContext::modeled_request()],
/// [InterceptorContext::tx_request()] and
/// [InterceptorContext::modeled_response()] are **ALWAYS** available.
/// The [InterceptorContext::tx_response()] is available if a
/// response was received by the service for this attempt.
/// In the event of retries, the `InterceptorContext` will not include
/// changes made in previous attempts (e.g. by request signers or other
/// interceptors).
///
/// **Error Behavior:** Errors raised by this hook will be stored
/// until all interceptors have had their `after_attempt` invoked.
/// If multiple `after_execution` methods raise errors, the latest
/// will be used and earlier ones will be logged and dropped. If the
/// retry strategy determines that the execution is retryable,
/// execution will then jump to `before_attempt`. Otherwise,
/// execution will jump to `modify_before_attempt_completion` with the
/// raised error as the [InterceptorContext::modeled_response()].
fn read_after_attempt(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called when an execution is completed.
/// This method has the ability to modify and return a new
/// output message or error matching the currently - executing
/// operation.
///
/// **When:** This will **ALWAYS** be called once per execution.
///
/// **Available Information:** The [InterceptorContext::modeled_request()]
/// and [InterceptorContext::modeled_response()] are **ALWAYS** available. The
/// [InterceptorContext::tx_request()]
/// and [InterceptorContext::tx_response()] are available if the
/// execution proceeded far enough for them to be generated.
///
/// **Error Behavior:** If errors are raised by this
/// hook , execution will jump to `after_attempt` with
/// the raised error as the [InterceptorContext::modeled_response()].
///
/// **Return Constraints:** Any output message returned by this
/// hook MUST match the operation being invoked. Any error type can be
/// returned , replacing the response currently in the context.
fn modify_before_completion(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
/// A hook called when an execution is completed.
///
/// **When:** This will **ALWAYS** be called once per execution. The duration
/// between invocation of this hook and `before_execution` is very
/// close to the full duration of the execution.
///
/// **Available Information:** The [InterceptorContext::modeled_request()]
/// and [InterceptorContext::modeled_response()] are **ALWAYS** available. The
/// [InterceptorContext::tx_request()] and
/// [InterceptorContext::tx_response()] are available if the
/// execution proceeded far enough for them to be generated.
///
/// **Error Behavior:** Errors raised by this hook will be stored
/// until all interceptors have had their `after_execution` invoked.
/// The error will then be treated as the
/// [InterceptorContext::modeled_response()] to the customer. If multiple
/// `after_execution` methods raise errors , the latest will be
/// used and earlier ones will be logged and dropped.
fn read_after_execution(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
let _ctx = context;
let _cfg = cfg;
Ok(())
}
}
pub struct Interceptors<ModReq, TxReq, TxRes, ModRes> {
client_interceptors: Vec<Box<dyn Interceptor<ModReq, TxReq, TxRes, ModRes>>>,
operation_interceptors: Vec<Box<dyn Interceptor<ModReq, TxReq, TxRes, ModRes>>>,
}
impl<ModReq, TxReq, TxRes, ModRes> Default for Interceptors<ModReq, TxReq, TxRes, ModRes> {
fn default() -> Self {
Self {
client_interceptors: Vec::new(),
operation_interceptors: Vec::new(),
}
}
}
impl<ModReq, TxReq, TxRes, ModRes> Interceptors<ModReq, TxReq, TxRes, ModRes> {
pub fn new() -> Self {
Self::default()
}
pub fn with_client_interceptor(
&mut self,
interceptor: impl Interceptor<ModReq, TxReq, TxRes, ModRes> + 'static,
) -> &mut Self {
self.client_interceptors.push(Box::new(interceptor));
self
}
pub fn with_operation_interceptor(
&mut self,
interceptor: impl Interceptor<ModReq, TxReq, TxRes, ModRes> + 'static,
) -> &mut Self {
self.operation_interceptors.push(Box::new(interceptor));
self
}
fn all_interceptors_mut(
&mut self,
) -> impl Iterator<Item = &mut Box<dyn Interceptor<ModReq, TxReq, TxRes, ModRes>>> {
self.client_interceptors
.iter_mut()
.chain(self.operation_interceptors.iter_mut())
}
pub fn client_read_before_execution(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.client_interceptors.iter_mut() {
interceptor.read_before_execution(context, cfg)?;
}
Ok(())
}
pub fn operation_read_before_execution(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.operation_interceptors.iter_mut() {
interceptor.read_before_execution(context, cfg)?;
}
Ok(())
}
pub fn modify_before_serialization(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.modify_before_serialization(context, cfg)?;
}
Ok(())
}
pub fn read_before_serialization(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.read_before_serialization(context, cfg)?;
}
Ok(())
}
pub fn read_after_serialization(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.read_after_serialization(context, cfg)?;
}
Ok(())
}
pub fn modify_before_retry_loop(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.modify_before_retry_loop(context, cfg)?;
}
Ok(())
}
pub fn read_before_attempt(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.read_before_attempt(context, cfg)?;
}
Ok(())
}
pub fn modify_before_signing(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.modify_before_signing(context, cfg)?;
}
Ok(())
}
pub fn read_before_signing(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.read_before_signing(context, cfg)?;
}
Ok(())
}
pub fn read_after_signing(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.read_after_signing(context, cfg)?;
}
Ok(())
}
pub fn modify_before_transmit(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.modify_before_transmit(context, cfg)?;
}
Ok(())
}
pub fn read_before_transmit(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.read_before_transmit(context, cfg)?;
}
Ok(())
}
pub fn read_after_transmit(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.read_after_transmit(context, cfg)?;
}
Ok(())
}
pub fn modify_before_deserialization(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.modify_before_deserialization(context, cfg)?;
}
Ok(())
}
pub fn read_before_deserialization(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.read_before_deserialization(context, cfg)?;
}
Ok(())
}
pub fn read_after_deserialization(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.read_after_deserialization(context, cfg)?;
}
Ok(())
}
pub fn modify_before_attempt_completion(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.modify_before_attempt_completion(context, cfg)?;
}
Ok(())
}
pub fn read_after_attempt(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.read_after_attempt(context, cfg)?;
}
Ok(())
}
pub fn modify_before_completion(
&mut self,
context: &mut InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.modify_before_completion(context, cfg)?;
}
Ok(())
}
pub fn read_after_execution(
&mut self,
context: &InterceptorContext<ModReq, TxReq, TxRes, ModRes>,
cfg: &mut ConfigBag,
) -> Result<(), InterceptorError> {
for interceptor in self.all_interceptors_mut() {
interceptor.read_after_execution(context, cfg)?;
}
Ok(())
}
}

View File

@ -1,126 +0,0 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
use super::InterceptorError;
/// A container for the data currently available to an interceptor.
pub struct InterceptorContext<ModReq, TxReq, TxRes, ModRes> {
modeled_request: ModReq,
tx_request: Option<TxReq>,
modeled_response: Option<ModRes>,
tx_response: Option<TxRes>,
}
// TODO(interceptors) we could use types to ensure that people calling methods on interceptor context can't access
// field that haven't been set yet.
impl<ModReq, TxReq, TxRes, ModRes> InterceptorContext<ModReq, TxReq, TxRes, ModRes> {
pub fn new(request: ModReq) -> Self {
Self {
modeled_request: request,
tx_request: None,
tx_response: None,
modeled_response: None,
}
}
/// Retrieve the modeled request for the operation being invoked.
pub fn modeled_request(&self) -> &ModReq {
&self.modeled_request
}
/// Retrieve the modeled request for the operation being invoked.
pub fn modeled_request_mut(&mut self) -> &mut ModReq {
&mut self.modeled_request
}
/// Retrieve the transmittable request for the operation being invoked.
/// This will only be available once request marshalling has completed.
pub fn tx_request(&self) -> Result<&TxReq, InterceptorError> {
self.tx_request
.as_ref()
.ok_or_else(InterceptorError::invalid_tx_request_access)
}
/// Retrieve the transmittable request for the operation being invoked.
/// This will only be available once request marshalling has completed.
pub fn tx_request_mut(&mut self) -> Result<&mut TxReq, InterceptorError> {
self.tx_request
.as_mut()
.ok_or_else(InterceptorError::invalid_tx_request_access)
}
/// Retrieve the response to the transmittable request for the operation
/// being invoked. This will only be available once transmission has
/// completed.
pub fn tx_response(&self) -> Result<&TxRes, InterceptorError> {
self.tx_response
.as_ref()
.ok_or_else(InterceptorError::invalid_tx_response_access)
}
/// Retrieve the response to the transmittable request for the operation
/// being invoked. This will only be available once transmission has
/// completed.
pub fn tx_response_mut(&mut self) -> Result<&mut TxRes, InterceptorError> {
self.tx_response
.as_mut()
.ok_or_else(InterceptorError::invalid_tx_response_access)
}
/// Retrieve the response to the customer. This will only be available
/// once the `tx_response` has been unmarshalled or the
/// attempt/execution has failed.
pub fn modeled_response(&self) -> Result<&ModRes, InterceptorError> {
self.modeled_response
.as_ref()
.ok_or_else(InterceptorError::invalid_modeled_response_access)
}
/// Retrieve the response to the customer. This will only be available
/// once the `tx_response` has been unmarshalled or the
/// attempt/execution has failed.
pub fn modeled_response_mut(&mut self) -> Result<&mut ModRes, InterceptorError> {
self.modeled_response
.as_mut()
.ok_or_else(InterceptorError::invalid_modeled_response_access)
}
// There is no set_modeled_request method because that can only be set once, during context construction
pub fn set_tx_request(&mut self, transmit_request: TxReq) {
if self.tx_request.is_some() {
panic!("Called set_tx_request but a transmit_request was already set. This is a bug, pleases report it.");
}
self.tx_request = Some(transmit_request);
}
pub fn set_tx_response(&mut self, transmit_response: TxRes) {
if self.tx_response.is_some() {
panic!("Called set_tx_response but a transmit_response was already set. This is a bug, pleases report it.");
}
self.tx_response = Some(transmit_response);
}
pub fn set_modeled_response(&mut self, modeled_response: ModRes) {
if self.modeled_response.is_some() {
panic!("Called set_modeled_response but a modeled_response was already set. This is a bug, pleases report it.");
}
self.modeled_response = Some(modeled_response);
}
pub fn into_responses(self) -> Result<(ModRes, TxRes), InterceptorError> {
let mod_res = self
.modeled_response
.ok_or_else(InterceptorError::invalid_modeled_response_access)?;
let tx_res = self
.tx_response
.ok_or_else(InterceptorError::invalid_tx_response_access)?;
Ok((mod_res, tx_res))
}
}

View File

@ -12,16 +12,11 @@
//! Basic types for the new smithy client orchestrator.
/// Smithy runtime for client orchestration.
pub mod client;
/// A typemap for storing configuration.
pub mod config_bag;
/// Smithy interceptors for smithy clients.
///
/// Interceptors are lifecycle hooks that can read/modify requests and responses.
pub mod interceptors;
/// Smithy code related to retry handling and token bucket.
///
/// This code defines when and how failed requests should be retried. It also defines the behavior
/// used to limit the rate that requests are sent.
pub mod retries;
/// Runtime plugin type definitions.
pub mod runtime_plugin;
/// Utilities for type erasure.
pub mod type_erasure;

View File

@ -0,0 +1,149 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
use std::any::Any;
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
/// A [`TypeErasedBox`] with type information tracked via generics at compile-time
///
/// `TypedBox` is used to transition to/from a `TypeErasedBox`. A `TypedBox<T>` can only
/// be created from a `T` or from a `TypeErasedBox` value that _is a_ `T`. Therefore, it can
/// be assumed to be a `T` even though the underlying storage is still a `TypeErasedBox`.
/// Since the `T` is only used in `PhantomData`, it gets compiled down to just a `TypeErasedBox`.
///
/// The orchestrator uses `TypeErasedBox` to avoid the complication of six or more generic parameters
/// and to avoid the monomorphization that brings with it. This `TypedBox` will primarily be useful
/// for operation-specific or service-specific interceptors that need to operate on the actual
/// input/output/error types.
#[derive(Debug)]
pub struct TypedBox<T> {
inner: TypeErasedBox,
_phantom: PhantomData<T>,
}
impl<T> TypedBox<T>
where
T: Send + Sync + 'static,
{
// Creates a new `TypedBox`.
pub fn new(inner: T) -> Self {
Self {
inner: TypeErasedBox::new(Box::new(inner) as _),
_phantom: Default::default(),
}
}
// Tries to create a `TypedBox<T>` from a `TypeErasedBox`.
//
// If the `TypedBox<T>` can't be created due to the `TypeErasedBox`'s value consisting
// of another type, then the original `TypeErasedBox` will be returned in the `Err` variant.
pub fn assume_from(type_erased: TypeErasedBox) -> Result<TypedBox<T>, TypeErasedBox> {
if type_erased.downcast_ref::<T>().is_some() {
Ok(TypedBox {
inner: type_erased,
_phantom: Default::default(),
})
} else {
Err(type_erased)
}
}
/// Converts the `TypedBox<T>` back into `T`.
pub fn unwrap(self) -> T {
*self.inner.downcast::<T>().expect("type checked")
}
/// Converts the `TypedBox<T>` into a `TypeErasedBox`.
pub fn erase(self) -> TypeErasedBox {
self.inner
}
}
impl<T: 'static> Deref for TypedBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.inner.downcast_ref().expect("type checked")
}
}
impl<T: 'static> DerefMut for TypedBox<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.inner.downcast_mut().expect("type checked")
}
}
/// A new-type around `Box<dyn Any + Send + Sync>`
#[derive(Debug)]
pub struct TypeErasedBox {
inner: Box<dyn Any + Send + Sync>,
}
impl TypeErasedBox {
// Creates a new `TypeErasedBox`.
pub fn new(inner: Box<dyn Any + Send + Sync>) -> Self {
Self { inner }
}
// Downcast into a `Box<T>`, or return `Self` if it is not a `T`.
pub fn downcast<T: 'static>(self) -> Result<Box<T>, Self> {
match self.inner.downcast() {
Ok(t) => Ok(t),
Err(s) => Err(Self { inner: s }),
}
}
/// Downcast as a `&T`, or return `None` if it is not a `T`.
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
self.inner.downcast_ref()
}
/// Downcast as a `&mut T`, or return `None` if it is not a `T`.
pub fn downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.inner.downcast_mut()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct Foo(&'static str);
#[derive(Debug)]
struct Bar(isize);
#[test]
fn test() {
let foo = TypedBox::new(Foo("1"));
let bar = TypedBox::new(Bar(2));
let mut foo_erased = foo.erase();
foo_erased
.downcast_mut::<Foo>()
.expect("I know its a Foo")
.0 = "3";
let bar_erased = bar.erase();
let bar_erased = TypedBox::<Foo>::assume_from(bar_erased).expect_err("it's not a Foo");
let mut bar = TypedBox::<Bar>::assume_from(bar_erased).expect("it's a Bar");
assert_eq!(2, bar.0);
bar.0 += 1;
let bar = bar.unwrap();
assert_eq!(3, bar.0);
assert!(foo_erased.downcast_ref::<Bar>().is_none());
assert!(foo_erased.downcast_mut::<Bar>().is_none());
let mut foo_erased = foo_erased.downcast::<Bar>().expect_err("it's not a Bar");
assert_eq!("3", foo_erased.downcast_ref::<Foo>().expect("it's a Foo").0);
foo_erased.downcast_mut::<Foo>().expect("it's a Foo").0 = "4";
let foo = *foo_erased.downcast::<Foo>().expect("it's a Foo");
assert_eq!("4", foo.0);
}
}

View File

@ -12,10 +12,13 @@ publish = false
[dependencies]
aws-smithy-http = { path = "../aws-smithy-http" }
aws-smithy-types = { path = "../aws-smithy-types" }
aws-smithy-runtime-api = { path = "../aws-smithy-runtime-api" }
aws-smithy-types = { path = "../aws-smithy-types" }
bytes = "1"
http = "0.2.8"
http-body = "0.4.5"
pin-utils = "0.1.0"
tracing = "0.1"
[package.metadata.docs.rs]
all-features = true

View File

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

View File

@ -0,0 +1,157 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
use self::auth::orchestrate_auth;
use crate::client::orchestrator::http::read_body;
use crate::client::orchestrator::phase::Phase;
use aws_smithy_http::result::SdkError;
use aws_smithy_runtime_api::client::interceptors::context::{Error, Input, Output};
use aws_smithy_runtime_api::client::interceptors::{InterceptorContext, Interceptors};
use aws_smithy_runtime_api::client::orchestrator::{
BoxError, ConfigBagAccessors, HttpRequest, HttpResponse,
};
use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugins;
use aws_smithy_runtime_api::config_bag::ConfigBag;
use tracing::{debug_span, Instrument};
mod auth;
mod http;
pub(self) mod phase;
pub async fn invoke(
input: Input,
interceptors: &mut Interceptors<HttpRequest, HttpResponse>,
runtime_plugins: &RuntimePlugins,
cfg: &mut ConfigBag,
) -> Result<Output, SdkError<Error, HttpResponse>> {
let context = Phase::construction(InterceptorContext::new(input))
// Client configuration
.include(|_| runtime_plugins.apply_client_configuration(cfg))?
.include(|ctx| interceptors.client_read_before_execution(ctx, cfg))?
// Operation configuration
.include(|_| runtime_plugins.apply_operation_configuration(cfg))?
.include(|ctx| interceptors.operation_read_before_execution(ctx, cfg))?
// Before serialization
.include(|ctx| interceptors.read_before_serialization(ctx, cfg))?
.include_mut(|ctx| interceptors.modify_before_serialization(ctx, cfg))?
// Serialization
.include_mut(|ctx| {
let request_serializer = cfg.request_serializer();
let request = request_serializer.serialize_input(ctx.input(), cfg)?;
ctx.set_request(request);
Result::<(), BoxError>::Ok(())
})?
// After serialization
.include(|ctx| interceptors.read_after_serialization(ctx, cfg))?
// Before retry loop
.include_mut(|ctx| interceptors.modify_before_retry_loop(ctx, cfg))?
.finish();
{
let retry_strategy = cfg.retry_strategy();
match retry_strategy.should_attempt_initial_request(cfg) {
// Yes, let's make a request
Ok(_) => {}
// No, we shouldn't make a request because...
Err(err) => return Err(Phase::dispatch(context).fail(err)),
}
}
let mut context = context;
let handling_phase = loop {
let dispatch_phase = Phase::dispatch(context);
context = make_an_attempt(dispatch_phase, cfg, interceptors)
.await?
.include(|ctx| interceptors.read_after_attempt(ctx, cfg))?
.include_mut(|ctx| interceptors.modify_before_attempt_completion(ctx, cfg))?
.finish();
let retry_strategy = cfg.retry_strategy();
match retry_strategy.should_attempt_retry(&context, cfg) {
// Yes, let's retry the request
Ok(true) => continue,
// No, this request shouldn't be retried
Ok(false) => {}
// I couldn't determine if the request should be retried because an error occurred.
Err(err) => {
return Err(Phase::response_handling(context).fail(err));
}
}
let handling_phase = Phase::response_handling(context)
.include_mut(|ctx| interceptors.modify_before_completion(ctx, cfg))?;
let trace_probe = cfg.trace_probe();
trace_probe.dispatch_events(cfg);
break handling_phase.include(|ctx| interceptors.read_after_execution(ctx, cfg))?;
};
handling_phase.finalize()
}
// Making an HTTP request can fail for several reasons, but we still need to
// call lifecycle events when that happens. Therefore, we define this
// `make_an_attempt` function to make error handling simpler.
async fn make_an_attempt(
dispatch_phase: Phase,
cfg: &mut ConfigBag,
interceptors: &mut Interceptors<HttpRequest, HttpResponse>,
) -> Result<Phase, SdkError<Error, HttpResponse>> {
let dispatch_phase = dispatch_phase
.include(|ctx| interceptors.read_before_attempt(ctx, cfg))?
.include_mut(|ctx| {
let request = ctx.request_mut().expect("request has been set");
let endpoint_resolver = cfg.endpoint_resolver();
endpoint_resolver.resolve_and_apply_endpoint(request, cfg)
})?
.include_mut(|ctx| interceptors.modify_before_signing(ctx, cfg))?
.include(|ctx| interceptors.read_before_signing(ctx, cfg))?;
let dispatch_phase = orchestrate_auth(dispatch_phase, cfg).await?;
let mut context = dispatch_phase
.include(|ctx| interceptors.read_after_signing(ctx, cfg))?
.include_mut(|ctx| interceptors.modify_before_transmit(ctx, cfg))?
.include(|ctx| interceptors.read_before_transmit(ctx, cfg))?
.finish();
// The connection consumes the request but we need to keep a copy of it
// within the interceptor context, so we clone it here.
let call_result = {
let tx_req = context.request_mut().expect("request has been set");
let connection = cfg.connection();
connection.call(tx_req, cfg).await
};
let mut context = Phase::dispatch(context)
.include_mut(move |ctx| {
ctx.set_response(call_result?);
Result::<(), BoxError>::Ok(())
})?
.include(|ctx| interceptors.read_after_transmit(ctx, cfg))?
.include_mut(|ctx| interceptors.modify_before_deserialization(ctx, cfg))?
.include(|ctx| interceptors.read_before_deserialization(ctx, cfg))?
.finish();
let output_or_error = {
let response = context.response_mut().expect("response has been set");
let response_deserializer = cfg.response_deserializer();
match response_deserializer.deserialize_streaming(response) {
Some(output_or_error) => Ok(output_or_error),
None => read_body(response)
.instrument(debug_span!("read_body"))
.await
.map(|_| response_deserializer.deserialize_nonstreaming(response)),
}
};
Phase::response_handling(context)
.include_mut(move |ctx| {
ctx.set_output_or_error(output_or_error?);
Result::<(), BoxError>::Ok(())
})?
.include(|ctx| interceptors.read_after_deserialization(ctx, cfg))
}

View File

@ -0,0 +1,36 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
use super::phase::Phase;
use aws_smithy_http::result::SdkError;
use aws_smithy_runtime_api::client::interceptors::context::Error;
use aws_smithy_runtime_api::client::orchestrator::{BoxError, ConfigBagAccessors, HttpResponse};
use aws_smithy_runtime_api::config_bag::ConfigBag;
pub(super) async fn orchestrate_auth(
dispatch_phase: Phase,
cfg: &ConfigBag,
) -> Result<Phase, SdkError<Error, HttpResponse>> {
dispatch_phase.include_mut(|ctx| {
let params = cfg.auth_option_resolver_params();
let auth_options = cfg.auth_option_resolver().resolve_auth_options(params)?;
let identity_resolvers = cfg.identity_resolvers();
for option in auth_options {
let scheme_id = option.scheme_id();
if let Some(auth_scheme) = cfg.http_auth_schemes().scheme(scheme_id) {
let identity_resolver = auth_scheme.identity_resolver(identity_resolvers);
let request_signer = auth_scheme.request_signer();
let identity = identity_resolver.resolve_identity(cfg)?;
let request = ctx.request_mut()?;
request_signer.sign_request(request, &identity, cfg)?;
return Result::<_, BoxError>::Ok(());
}
}
Err("No auth scheme matched auth options. This is a bug. Please file an issue.".into())
})
}

View File

@ -0,0 +1,35 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
use aws_smithy_http::body::SdkBody;
use aws_smithy_runtime_api::client::orchestrator::HttpResponse;
use bytes::{Buf, Bytes};
use http_body::Body;
use pin_utils::pin_mut;
async fn body_to_bytes(body: SdkBody) -> Result<Bytes, <SdkBody as Body>::Error> {
let mut output = Vec::new();
pin_mut!(body);
while let Some(buf) = body.data().await {
let mut buf = buf?;
while buf.has_remaining() {
output.extend_from_slice(buf.chunk());
buf.advance(buf.chunk().len())
}
}
Ok(Bytes::from(output))
}
pub(crate) async fn read_body(response: &mut HttpResponse) -> Result<(), <SdkBody as Body>::Error> {
let mut body = SdkBody::taken();
std::mem::swap(&mut body, response.body_mut());
let bytes = body_to_bytes(body).await?;
let mut body = SdkBody::from(bytes);
std::mem::swap(&mut body, response.body_mut());
Ok(())
}

View File

@ -0,0 +1,119 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
use aws_smithy_http::result::{ConnectorError, SdkError};
use aws_smithy_runtime_api::client::interceptors::context::{Error, Output};
use aws_smithy_runtime_api::client::interceptors::InterceptorContext;
use aws_smithy_runtime_api::client::orchestrator::{BoxError, HttpRequest, HttpResponse};
#[derive(Copy, Clone, Eq, PartialEq)]
enum OrchestrationPhase {
Construction,
Dispatch,
ResponseHandling,
}
pub(super) struct Phase {
phase: OrchestrationPhase,
context: InterceptorContext<HttpRequest, HttpResponse>,
}
impl Phase {
pub(crate) fn construction(context: InterceptorContext<HttpRequest, HttpResponse>) -> Self {
Self::start(OrchestrationPhase::Construction, context)
}
pub(crate) fn dispatch(context: InterceptorContext<HttpRequest, HttpResponse>) -> Self {
Self::start(OrchestrationPhase::Dispatch, context)
}
pub(crate) fn response_handling(
context: InterceptorContext<HttpRequest, HttpResponse>,
) -> Self {
Self::start(OrchestrationPhase::ResponseHandling, context)
}
fn start(
phase: OrchestrationPhase,
context: InterceptorContext<HttpRequest, HttpResponse>,
) -> Self {
match phase {
OrchestrationPhase::Construction => {}
OrchestrationPhase::Dispatch => debug_assert!(context.request().is_ok()),
OrchestrationPhase::ResponseHandling => debug_assert!(context.response().is_ok()),
}
Self { phase, context }
}
pub(crate) fn include_mut<E: Into<BoxError>>(
mut self,
c: impl FnOnce(&mut InterceptorContext<HttpRequest, HttpResponse>) -> Result<(), E>,
) -> Result<Self, SdkError<Error, HttpResponse>> {
match c(&mut self.context) {
Ok(_) => Ok(self),
Err(e) => Err(self.fail(e)),
}
}
pub(crate) fn include<E: Into<BoxError>>(
self,
c: impl FnOnce(&InterceptorContext<HttpRequest, HttpResponse>) -> Result<(), E>,
) -> Result<Self, SdkError<Error, HttpResponse>> {
match c(&self.context) {
Ok(_) => Ok(self),
Err(e) => Err(self.fail(e)),
}
}
pub(crate) fn fail(self, e: impl Into<BoxError>) -> SdkError<Error, HttpResponse> {
self.into_sdk_error(e.into())
}
pub(crate) fn finalize(self) -> Result<Output, SdkError<Error, HttpResponse>> {
debug_assert!(self.phase == OrchestrationPhase::ResponseHandling);
let (_input, output_or_error, _request, response) = self.context.into_parts();
match output_or_error {
Some(output_or_error) => match output_or_error {
Ok(output) => Ok(output),
Err(error) => Err(SdkError::service_error(
error,
response.expect("response must be set by this point"),
)),
},
None => unreachable!("phase can't get this far without bubbling up a failure"),
}
}
fn into_sdk_error(self, e: BoxError) -> SdkError<Error, HttpResponse> {
let e = match e.downcast::<ConnectorError>() {
Ok(connector_error) => {
debug_assert!(
self.phase == OrchestrationPhase::Dispatch,
"connector errors should only occur during the dispatch phase"
);
return SdkError::dispatch_failure(*connector_error);
}
Err(e) => e,
};
let (_input, output_or_error, _request, response) = self.context.into_parts();
match self.phase {
OrchestrationPhase::Construction => SdkError::construction_failure(e),
OrchestrationPhase::Dispatch => {
if let Some(response) = response {
SdkError::response_error(e, response)
} else {
SdkError::dispatch_failure(ConnectorError::other(e, None))
}
}
OrchestrationPhase::ResponseHandling => match (response, output_or_error) {
(Some(response), Some(Err(error))) => SdkError::service_error(error, response),
(Some(response), _) => SdkError::response_error(e, response),
_ => unreachable!("response handling phase at least has a response"),
},
}
}
pub(crate) fn finish(self) -> InterceptorContext<HttpRequest, HttpResponse> {
self.context
}
}

View File

@ -10,172 +10,4 @@
rust_2018_idioms
)]
use aws_smithy_runtime_api::config_bag::ConfigBag;
use aws_smithy_runtime_api::interceptors::{InterceptorContext, Interceptors};
use aws_smithy_runtime_api::runtime_plugin::RuntimePlugins;
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub type BoxFallibleFut<T> = Pin<Box<dyn Future<Output = Result<T, BoxError>>>>;
pub trait TraceProbe: Send + Sync + Debug {
fn dispatch_events(&self, cfg: &ConfigBag) -> BoxFallibleFut<()>;
}
pub trait RequestSerializer<In, TxReq>: Send + Sync + Debug {
fn serialize_request(&self, req: &mut In, cfg: &ConfigBag) -> Result<TxReq, BoxError>;
}
pub trait ResponseDeserializer<TxRes, Out>: Send + Sync + Debug {
fn deserialize_response(&self, res: &mut TxRes, cfg: &ConfigBag) -> Result<Out, BoxError>;
}
pub trait Connection<TxReq, TxRes>: Send + Sync + Debug {
fn call(&self, req: &mut TxReq, cfg: &ConfigBag) -> BoxFallibleFut<TxRes>;
}
pub trait RetryStrategy<Out>: Send + Sync + Debug {
fn should_retry(&self, res: &Out, cfg: &ConfigBag) -> Result<bool, BoxError>;
}
pub trait AuthOrchestrator<Req>: Send + Sync + Debug {
fn auth_request(&self, req: &mut Req, cfg: &ConfigBag) -> Result<(), BoxError>;
}
pub trait EndpointOrchestrator<Req>: Send + Sync + Debug {
fn resolve_and_apply_endpoint(&self, req: &mut Req, cfg: &ConfigBag) -> Result<(), BoxError>;
// TODO(jdisanti) The EP Orc and Auth Orc need to share info on auth schemes but I'm not sure how that should happen
fn resolve_auth_schemes(&self) -> Result<Vec<String>, BoxError>;
}
/// `In`: The input message e.g. `ListObjectsRequest`
/// `Req`: The transport request message e.g. `http::Request<SmithyBody>`
/// `Res`: The transport response message e.g. `http::Response<SmithyBody>`
/// `Out`: The output message. A `Result` containing either:
/// - The 'success' output message e.g. `ListObjectsResponse`
/// - The 'failure' output message e.g. `NoSuchBucketException`
pub async fn invoke<In, Req, Res, T>(
input: In,
interceptors: &mut Interceptors<In, Req, Res, Result<T, BoxError>>,
runtime_plugins: &RuntimePlugins,
cfg: &mut ConfigBag,
) -> Result<T, BoxError>
where
// The input must be Clone in case of retries
In: Clone + 'static,
Req: 'static,
Res: 'static,
T: 'static,
{
let mut ctx: InterceptorContext<In, Req, Res, Result<T, BoxError>> =
InterceptorContext::new(input);
runtime_plugins.apply_client_configuration(cfg)?;
interceptors.client_read_before_execution(&ctx, cfg)?;
runtime_plugins.apply_operation_configuration(cfg)?;
interceptors.operation_read_before_execution(&ctx, cfg)?;
interceptors.read_before_serialization(&ctx, cfg)?;
interceptors.modify_before_serialization(&mut ctx, cfg)?;
let request_serializer = cfg
.get::<Box<dyn RequestSerializer<In, Req>>>()
.ok_or("missing serializer")?;
let req = request_serializer.serialize_request(ctx.modeled_request_mut(), cfg)?;
ctx.set_tx_request(req);
interceptors.read_after_serialization(&ctx, cfg)?;
interceptors.modify_before_retry_loop(&mut ctx, cfg)?;
loop {
make_an_attempt(&mut ctx, cfg, interceptors).await?;
interceptors.read_after_attempt(&ctx, cfg)?;
interceptors.modify_before_attempt_completion(&mut ctx, cfg)?;
let retry_strategy = cfg
.get::<Box<dyn RetryStrategy<Result<T, BoxError>>>>()
.ok_or("missing retry strategy")?;
let mod_res = ctx
.modeled_response()
.expect("it's set during 'make_an_attempt'");
if retry_strategy.should_retry(mod_res, cfg)? {
continue;
}
interceptors.modify_before_completion(&mut ctx, cfg)?;
let trace_probe = cfg
.get::<Box<dyn TraceProbe>>()
.ok_or("missing trace probes")?;
trace_probe.dispatch_events(cfg);
interceptors.read_after_execution(&ctx, cfg)?;
break;
}
let (modeled_response, _) = ctx.into_responses()?;
modeled_response
}
// Making an HTTP request can fail for several reasons, but we still need to
// call lifecycle events when that happens. Therefore, we define this
// `make_an_attempt` function to make error handling simpler.
async fn make_an_attempt<In, Req, Res, T>(
ctx: &mut InterceptorContext<In, Req, Res, Result<T, BoxError>>,
cfg: &mut ConfigBag,
interceptors: &mut Interceptors<In, Req, Res, Result<T, BoxError>>,
) -> Result<(), BoxError>
where
In: Clone + 'static,
Req: 'static,
Res: 'static,
T: 'static,
{
interceptors.read_before_attempt(ctx, cfg)?;
let tx_req_mut = ctx.tx_request_mut().expect("tx_request has been set");
let endpoint_orchestrator = cfg
.get::<Box<dyn EndpointOrchestrator<Req>>>()
.ok_or("missing endpoint orchestrator")?;
endpoint_orchestrator.resolve_and_apply_endpoint(tx_req_mut, cfg)?;
interceptors.modify_before_signing(ctx, cfg)?;
interceptors.read_before_signing(ctx, cfg)?;
let tx_req_mut = ctx.tx_request_mut().expect("tx_request has been set");
let auth_orchestrator = cfg
.get::<Box<dyn AuthOrchestrator<Req>>>()
.ok_or("missing auth orchestrator")?;
auth_orchestrator.auth_request(tx_req_mut, cfg)?;
interceptors.read_after_signing(ctx, cfg)?;
interceptors.modify_before_transmit(ctx, cfg)?;
interceptors.read_before_transmit(ctx, cfg)?;
// The connection consumes the request but we need to keep a copy of it
// within the interceptor context, so we clone it here.
let res = {
let tx_req = ctx.tx_request_mut().expect("tx_request has been set");
let connection = cfg
.get::<Box<dyn Connection<Req, Res>>>()
.ok_or("missing connector")?;
connection.call(tx_req, cfg).await?
};
ctx.set_tx_response(res);
interceptors.read_after_transmit(ctx, cfg)?;
interceptors.modify_before_deserialization(ctx, cfg)?;
interceptors.read_before_deserialization(ctx, cfg)?;
let tx_res = ctx.tx_response_mut().expect("tx_response has been set");
let response_deserializer = cfg
.get::<Box<dyn ResponseDeserializer<Res, Result<T, BoxError>>>>()
.ok_or("missing response deserializer")?;
let res = response_deserializer.deserialize_response(tx_res, cfg)?;
ctx.set_modeled_response(res);
interceptors.read_after_deserialization(ctx, cfg)?;
Ok(())
}
pub mod client;

View File

@ -6,7 +6,6 @@
use aws_smithy_json::deserialize::token::skip_value;
use aws_smithy_json::deserialize::{error::DeserializeError, json_token_iter, Token};
use aws_smithy_types::error::metadata::{Builder as ErrorMetadataBuilder, ErrorMetadata};
use bytes::Bytes;
use http::header::ToStrError;
use http::{HeaderMap, HeaderValue};
use std::borrow::Cow;
@ -83,10 +82,10 @@ fn error_type_from_header(headers: &HeaderMap<HeaderValue>) -> Result<Option<&st
}
pub fn parse_error_metadata(
payload: &Bytes,
payload: &[u8],
headers: &HeaderMap<HeaderValue>,
) -> Result<ErrorMetadataBuilder, DeserializeError> {
let ErrorBody { code, message } = parse_error_body(payload.as_ref())?;
let ErrorBody { code, message } = parse_error_body(payload)?;
let mut err_builder = ErrorMetadata::builder();
if let Some(code) = error_type_from_header(headers)

View File

@ -13,10 +13,11 @@ include(":codegen-server:python")
include(":codegen-server-test")
include(":codegen-server-test:python")
include(":rust-runtime")
include(":aws:sdk-codegen")
include(":aws:sdk-adhoc-test")
include(":aws:sdk")
include(":aws:rust-runtime")
include(":aws:sdk")
include(":aws:sdk-adhoc-test")
include(":aws:sdk-codegen")
include(":aws:sra-test")
pluginManagement {
val smithyGradlePluginVersion: String by settings