From ddcf6d32fff705e9179966d85cf6b9aa0546b910 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Mon, 27 Mar 2023 16:15:36 -0700 Subject: [PATCH] Orchestrator `ResponseDeserializer` codegen and auth (#2494) * Convert interceptor fns to macro invocations Co-authored-by: John DiSanti * Add unmodeled error to ServiceError and bring in EventLog Co-authored-by: John DiSanti * 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 --- aws/rust-runtime/aws-http/src/request_id.rs | 6 + .../aws-inlineable/src/s3_request_id.rs | 6 + aws/sdk-adhoc-test/build.gradle.kts | 4 +- .../smithy/rustsdk/BaseRequestIdDecorator.kt | 4 +- .../rustsdk/HttpRequestChecksumDecorator.kt | 9 +- .../rustsdk/HttpResponseChecksumDecorator.kt | 9 +- .../rustsdk/customize/s3/S3Decorator.kt | 8 +- aws/sdk/build.gradle.kts | 3 +- .../aws-smithy-runtime-test/src/auth.rs | 24 + .../aws-smithy-runtime-test/src/conn.rs | 44 + .../aws-smithy-runtime-test/src/de.rs | 35 + .../aws-smithy-runtime-test/src/endpoints.rs | 24 + .../aws-smithy-runtime-test/src/main.rs | 70 ++ .../aws-smithy-runtime-test/src/retry.rs | 48 + .../aws-smithy-runtime-test/src/ser.rs | 32 + aws/sra-test/.gitignore | 1 + aws/sra-test/build.gradle.kts | 88 ++ .../client/smithy/ClientRustSettings.kt | 6 +- .../protocols/HttpBoundProtocolGenerator.kt | 270 +++++- .../codegen/core/rustlang/CargoDependency.kt | 2 + .../customize/OperationCustomization.kt | 8 +- .../codegen/core/smithy/protocols/AwsJson.kt | 5 +- .../codegen/core/smithy/protocols/AwsQuery.kt | 4 +- .../smithy/protocols/AwsQueryCompatible.kt | 10 +- .../codegen/core/smithy/protocols/Ec2Query.kt | 4 +- .../codegen/core/smithy/protocols/Protocol.kt | 2 +- .../codegen/core/smithy/protocols/RestJson.kt | 4 +- .../codegen/core/smithy/protocols/RestXml.kt | 4 +- rust-runtime/aws-smithy-http/src/result.rs | 15 + .../aws-smithy-runtime-api/Cargo.toml | 1 + .../external-types.toml | 4 + .../aws-smithy-runtime-api/src/client.rs | 22 + .../src/client/identity.rs | 67 ++ .../src/client/interceptors.rs | 622 +++++++++++++ .../src/client/interceptors/context.rs | 140 +++ .../src/{ => client}/interceptors/error.rs | 27 +- .../src/client/orchestrator.rs | 374 ++++++++ .../src/{ => client}/retries.rs | 0 .../src/{ => client}/retries/rate_limiting.rs | 0 .../retries/rate_limiting/error.rs | 0 .../retries/rate_limiting/token.rs | 4 +- .../retries/rate_limiting/token_bucket.rs | 0 .../src/{ => client}/runtime_plugin.rs | 0 .../src/interceptors.rs | 861 ------------------ .../src/interceptors/context.rs | 126 --- .../aws-smithy-runtime-api/src/lib.rs | 17 +- .../src/type_erasure.rs | 149 +++ rust-runtime/aws-smithy-runtime/Cargo.toml | 5 +- rust-runtime/aws-smithy-runtime/src/client.rs | 6 + .../src/client/orchestrator.rs | 157 ++++ .../src/client/orchestrator/auth.rs | 36 + .../src/client/orchestrator/http.rs | 35 + .../src/client/orchestrator/phase.rs | 119 +++ rust-runtime/aws-smithy-runtime/src/lib.rs | 170 +--- rust-runtime/inlineable/src/json_errors.rs | 5 +- settings.gradle.kts | 7 +- 56 files changed, 2444 insertions(+), 1259 deletions(-) create mode 100644 aws/sdk/integration-tests/aws-smithy-runtime-test/src/auth.rs create mode 100644 aws/sdk/integration-tests/aws-smithy-runtime-test/src/conn.rs create mode 100644 aws/sdk/integration-tests/aws-smithy-runtime-test/src/de.rs create mode 100644 aws/sdk/integration-tests/aws-smithy-runtime-test/src/endpoints.rs create mode 100644 aws/sdk/integration-tests/aws-smithy-runtime-test/src/main.rs create mode 100644 aws/sdk/integration-tests/aws-smithy-runtime-test/src/retry.rs create mode 100644 aws/sdk/integration-tests/aws-smithy-runtime-test/src/ser.rs create mode 100644 aws/sra-test/.gitignore create mode 100644 aws/sra-test/build.gradle.kts create mode 100644 rust-runtime/aws-smithy-runtime-api/src/client.rs create mode 100644 rust-runtime/aws-smithy-runtime-api/src/client/identity.rs create mode 100644 rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs create mode 100644 rust-runtime/aws-smithy-runtime-api/src/client/interceptors/context.rs rename rust-runtime/aws-smithy-runtime-api/src/{ => client}/interceptors/error.rs (95%) create mode 100644 rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs rename rust-runtime/aws-smithy-runtime-api/src/{ => client}/retries.rs (100%) rename rust-runtime/aws-smithy-runtime-api/src/{ => client}/retries/rate_limiting.rs (100%) rename rust-runtime/aws-smithy-runtime-api/src/{ => client}/retries/rate_limiting/error.rs (100%) rename rust-runtime/aws-smithy-runtime-api/src/{ => client}/retries/rate_limiting/token.rs (90%) rename rust-runtime/aws-smithy-runtime-api/src/{ => client}/retries/rate_limiting/token_bucket.rs (100%) rename rust-runtime/aws-smithy-runtime-api/src/{ => client}/runtime_plugin.rs (100%) delete mode 100644 rust-runtime/aws-smithy-runtime-api/src/interceptors.rs delete mode 100644 rust-runtime/aws-smithy-runtime-api/src/interceptors/context.rs create mode 100644 rust-runtime/aws-smithy-runtime-api/src/type_erasure.rs create mode 100644 rust-runtime/aws-smithy-runtime/src/client.rs create mode 100644 rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs create mode 100644 rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs create mode 100644 rust-runtime/aws-smithy-runtime/src/client/orchestrator/http.rs create mode 100644 rust-runtime/aws-smithy-runtime/src/client/orchestrator/phase.rs diff --git a/aws/rust-runtime/aws-http/src/request_id.rs b/aws/rust-runtime/aws-http/src/request_id.rs index c3f192722..7713328f7 100644 --- a/aws/rust-runtime/aws-http/src/request_id.rs +++ b/aws/rust-runtime/aws-http/src/request_id.rs @@ -58,6 +58,12 @@ impl RequestId for http::Response { } } +impl RequestId for HeaderMap { + fn request_id(&self) -> Option<&str> { + extract_request_id(self) + } +} + impl RequestId for Result where O: RequestId, diff --git a/aws/rust-runtime/aws-inlineable/src/s3_request_id.rs b/aws/rust-runtime/aws-inlineable/src/s3_request_id.rs index 909dcbcd7..f19ad434d 100644 --- a/aws/rust-runtime/aws-inlineable/src/s3_request_id.rs +++ b/aws/rust-runtime/aws-inlineable/src/s3_request_id.rs @@ -59,6 +59,12 @@ impl RequestIdExt for http::Response { } } +impl RequestIdExt for HeaderMap { + fn extended_request_id(&self) -> Option<&str> { + extract_extended_request_id(self) + } +} + impl RequestIdExt for Result where O: RequestIdExt, diff --git a/aws/sdk-adhoc-test/build.gradle.kts b/aws/sdk-adhoc-test/build.gradle.kts index c127b3ff9..98ad95310 100644 --- a/aws/sdk-adhoc-test/build.gradle.kts +++ b/aws/sdk-adhoc-test/build.gradle.kts @@ -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 diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/BaseRequestIdDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/BaseRequestIdDecorator.kt index b70bf419b..80a66b798 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/BaseRequestIdDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/BaseRequestIdDecorator.kt @@ -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), ) } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt index c1799c8bf..f2913a1aa 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt @@ -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, - ): List { - return baseCustomizations + HttpRequestChecksumCustomization(codegenContext, operation) + ): List = baseCustomizations.letIf(applies(codegenContext)) { + it + HttpRequestChecksumCustomization(codegenContext, operation) } } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpResponseChecksumDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpResponseChecksumDecorator.kt index f1a907a31..568abfa19 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpResponseChecksumDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpResponseChecksumDecorator.kt @@ -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, - ): List { - return baseCustomizations + HttpResponseChecksumCustomization(codegenContext, operation) + ): List = baseCustomizations.letIf(applies(codegenContext)) { + it + HttpResponseChecksumCustomization(codegenContext, operation) } } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt index 1dc373473..7e78f0d47 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt @@ -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, diff --git a/aws/sdk/build.gradle.kts b/aws/sdk/build.gradle.kts index c4aee974b..8fee361e9 100644 --- a/aws/sdk/build.gradle.kts +++ b/aws/sdk/build.gradle.kts @@ -99,7 +99,8 @@ fun generateSmithyBuild(services: AwsServices): String { "includeFluentClient": false, "renameErrors": false, "eventStreamAllowList": [$eventStreamAllowListMembers], - "enableNewCrateOrganizationScheme": true + "enableNewCrateOrganizationScheme": true, + "enableNewSmithyRuntime": false }, "service": "${service.service}", "module": "$moduleName", diff --git a/aws/sdk/integration-tests/aws-smithy-runtime-test/src/auth.rs b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/auth.rs new file mode 100644 index 000000000..632f7957a --- /dev/null +++ b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/auth.rs @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +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; + +#[derive(Debug)] +pub struct GetObjectAuthOrc {} + +impl GetObjectAuthOrc { + pub fn new() -> Self { + Self {} + } +} + +impl RuntimePlugin for GetObjectAuthOrc { + fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> { + // TODO(orchestrator) put an auth orchestrator in the bag + Ok(()) + } +} diff --git a/aws/sdk/integration-tests/aws-smithy-runtime-test/src/conn.rs b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/conn.rs new file mode 100644 index 000000000..84350fd7c --- /dev/null +++ b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/conn.rs @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_client::conns::Https; +use aws_smithy_client::hyper_ext::Adapter; +use aws_smithy_http::body::SdkBody; +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; + +#[derive(Debug)] +pub struct HyperConnection { + _adapter: Adapter, +} + +impl RuntimePlugin for HyperConnection { + fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> { + // TODO(orchestrator) put a connection in the bag + Ok(()) + } +} + +impl HyperConnection { + pub fn new() -> Self { + Self { + _adapter: Adapter::builder().build(aws_smithy_client::conns::https()), + } + } +} + +impl Connection for HyperConnection { + fn call( + &self, + _req: &mut HttpRequest, + _cfg: &ConfigBag, + ) -> BoxFallibleFut> { + todo!("hyper's connector wants to take ownership of req"); + // self.adapter.call(req) + } +} diff --git a/aws/sdk/integration-tests/aws-smithy-runtime-test/src/de.rs b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/de.rs new file mode 100644 index 000000000..522889f2e --- /dev/null +++ b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/de.rs @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +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; + +#[derive(Debug)] +pub struct GetObjectResponseDeserializer {} + +impl GetObjectResponseDeserializer { + pub fn new() -> Self { + Self {} + } +} + +impl RuntimePlugin for GetObjectResponseDeserializer { + fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> { + // TODO(orchestrator) put a deserializer in the bag + Ok(()) + } +} + +impl ResponseDeserializer for GetObjectResponseDeserializer { + fn deserialize_streaming(&self, _response: &mut HttpResponse) -> Option { + todo!() + } + + fn deserialize_nonstreaming(&self, _response: &HttpResponse) -> OutputOrError { + todo!() + } +} diff --git a/aws/sdk/integration-tests/aws-smithy-runtime-test/src/endpoints.rs b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/endpoints.rs new file mode 100644 index 000000000..0eb3ac40a --- /dev/null +++ b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/endpoints.rs @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_http::event_stream::BoxError; +use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin; +use aws_smithy_runtime_api::config_bag::ConfigBag; + +#[derive(Debug)] +pub struct GetObjectEndpointOrc {} + +impl GetObjectEndpointOrc { + pub fn new() -> Self { + Self {} + } +} + +impl RuntimePlugin for GetObjectEndpointOrc { + fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> { + // TODO(orchestrator) put an endpoint orchestrator in the bag + Ok(()) + } +} diff --git a/aws/sdk/integration-tests/aws-smithy-runtime-test/src/main.rs b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/main.rs new file mode 100644 index 000000000..7309a4caa --- /dev/null +++ b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/main.rs @@ -0,0 +1,70 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +mod auth; +mod conn; +mod de; +mod endpoints; +mod interceptors; +mod retry; +mod ser; + +use aws_sdk_s3::operation::get_object::{GetObjectError, GetObjectInput, GetObjectOutput}; +use aws_sdk_s3::types::ChecksumMode; +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::type_erasure::TypedBox; + +#[tokio::main] +async fn main() -> Result<(), BoxError> { + tracing_subscriber::fmt::init(); + + // Create the config we'll need to send the request + the request itself + let sdk_config = aws_config::load_from_env().await; + let _service_config = aws_sdk_s3::Config::from(&sdk_config); + + let input = TypedBox::new( + GetObjectInput::builder() + .bucket("zhessler-test-bucket") + .key("1000-lines.txt") + .checksum_mode(ChecksumMode::Enabled) + .build()?, + ) + .erase(); + + let mut runtime_plugins = RuntimePlugins::new(); + + // TODO(smithy-orchestrator-codegen) Make it so these are added by default for S3 + runtime_plugins + .with_client_plugin(auth::GetObjectAuthOrc::new()) + .with_client_plugin(conn::HyperConnection::new()) + // TODO(smithy-orchestrator-codegen) Make it so these are added by default for this S3 operation + .with_operation_plugin(endpoints::GetObjectEndpointOrc::new()) + .with_operation_plugin(retry::GetObjectRetryStrategy::new()) + .with_operation_plugin(de::GetObjectResponseDeserializer::new()) + .with_operation_plugin(ser::GetObjectInputSerializer::new()); + + let mut cfg = ConfigBag::base(); + let mut interceptors: Interceptors = Interceptors::new(); + let output = TypedBox::::assume_from( + invoke(input, &mut interceptors, &runtime_plugins, &mut cfg) + .await + .map_err(|err| { + err.map_service_error(|err| { + TypedBox::::assume_from(err) + .expect("error is GetObjectError") + .unwrap() + }) + })?, + ) + .expect("output is GetObjectOutput") + .unwrap(); + + dbg!(output); + Ok(()) +} diff --git a/aws/sdk/integration-tests/aws-smithy-runtime-test/src/retry.rs b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/retry.rs new file mode 100644 index 000000000..2a731a5f3 --- /dev/null +++ b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/retry.rs @@ -0,0 +1,48 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +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; + +#[derive(Debug)] +pub struct GetObjectRetryStrategy {} + +impl GetObjectRetryStrategy { + pub fn new() -> Self { + Self {} + } +} + +impl RuntimePlugin for GetObjectRetryStrategy { + fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> { + // TODO(orchestrator) put a retry strategy in the bag + Ok(()) + } +} + +impl RetryStrategy for GetObjectRetryStrategy { + fn should_attempt_initial_request(&self, _cfg: &ConfigBag) -> Result<(), BoxError> { + todo!() + } + + fn should_attempt_retry( + &self, + _context: &InterceptorContext, + _cfg: &ConfigBag, + ) -> Result { + todo!() + } +} + +// retry_classifier: Arc::new( +// |res: Result<&SdkSuccess, &SdkError>| -> RetryKind { +// let classifier = AwsResponseRetryClassifier::new(); +// classifier.classify_retry(res) +// }, +// ), diff --git a/aws/sdk/integration-tests/aws-smithy-runtime-test/src/ser.rs b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/ser.rs new file mode 100644 index 000000000..5aed30268 --- /dev/null +++ b/aws/sdk/integration-tests/aws-smithy-runtime-test/src/ser.rs @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +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; + +#[derive(Debug)] +pub struct GetObjectInputSerializer {} + +impl GetObjectInputSerializer { + pub fn new() -> Self { + Self {} + } +} + +impl RuntimePlugin for GetObjectInputSerializer { + fn configure(&self, _cfg: &mut ConfigBag) -> Result<(), BoxError> { + // TODO(orchestrator) put a serializer in the bag + Ok(()) + } +} + +impl RequestSerializer for GetObjectInputSerializer { + fn serialize_input(&self, _input: &Input, _cfg: &ConfigBag) -> Result { + todo!() + } +} diff --git a/aws/sra-test/.gitignore b/aws/sra-test/.gitignore new file mode 100644 index 000000000..388d181b4 --- /dev/null +++ b/aws/sra-test/.gitignore @@ -0,0 +1 @@ +/smithy-build.json diff --git a/aws/sra-test/build.gradle.kts b/aws/sra-test/build.gradle.kts new file mode 100644 index 000000000..d7cdeaa69 --- /dev/null +++ b/aws/sra-test/build.gradle.kts @@ -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 { + 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") } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/ClientRustSettings.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/ClientRustSettings.kt index f8ad5195b..dad5bf9d3 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/ClientRustSettings.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/ClientRustSettings.kt @@ -88,6 +88,8 @@ data class ClientCodegenConfig( val eventStreamAllowList: Set = 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 = emptySet() private const val defaultEnableNewCrateOrganizationScheme = true + private const val defaultEnableNewSmithyRuntime = false fun fromCodegenConfigAndNode(coreCodegenConfig: CoreCodegenConfig, node: Optional) = 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, ) } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/HttpBoundProtocolGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/HttpBoundProtocolGenerator.kt index 6a5aeaff8..18a2f0b32 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/HttpBoundProtocolGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/HttpBoundProtocolGenerator.kt @@ -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,17 +111,136 @@ 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 typeEraseResult(): RuntimeType = ProtocolFunctions.crossOperationFn("type_erase_result") { fnName -> + rustTemplate( + """ + pub(crate) fn $fnName(result: Result) -> 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, + 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, + ) { + val successCode = httpBindingResolver.httpTrait(operationShape).code + rustTemplate( + """ + 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 { + 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, + ) { + 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, + ) { + 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}(status, headers, body) + }; + #{type_erase_result}(parse_result) + """, + *orchestratorCodegenScope, + "parse_error" to parseError(operationShape, customizations), + "parse_response" to parseResponse(operationShape, customizations), + "BeforeParseResponse" to writable { + writeCustomizations(customizations, OperationSection.BeforeParseResponse(customizations, "response")) + }, + ) + } + + // TODO(enableNewSmithyRuntime): Delete this when cleaning up `enableNewSmithyRuntime` private fun RustWriter.renderNonStreamingTraits( operationName: String?, outputSymbol: Symbol, @@ -109,30 +248,37 @@ class HttpBoundProtocolTraitImplGenerator( customizations: List, ) { 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 !response.status().is_success() && response.status().as_u16() != $successCode { - #{parse_error}(response) + if !success && status != $successCode { + #{parse_error}(status, headers, body) } else { - #{parse_response}(response) + #{parse_response}(status, headers, body) } } }""", *codegenScope, - "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")) - }, + *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): 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()) { "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}") )? diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt index dddb75ecd..0f9929791 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt @@ -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") } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/customize/OperationCustomization.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/customize/OperationCustomization.kt index 7ab4586c3..e11936c70 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/customize/OperationCustomization.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/customize/OperationCustomization.kt @@ -52,6 +52,8 @@ sealed class OperationSection(name: String) : Section(name) { data class MutateOutput( override val customizations: List, 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, /** 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") /** diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsJson.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsJson.kt index 0476acdba..7812f917b 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsJson.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsJson.kt @@ -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, diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQuery.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQuery.kt index 880329d03..9d12cfbb6 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQuery.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQuery.kt @@ -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) } } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQueryCompatible.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQueryCompatible.kt index 200cb81c7..88fb69d01 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQueryCompatible.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQueryCompatible.kt @@ -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); diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/Ec2Query.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/Ec2Query.kt index 64128bac6..01a530d46 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/Ec2Query.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/Ec2Query.kt @@ -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) } } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/Protocol.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/Protocol.kt index 1b3e4b4a9..ec9b944e3 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/Protocol.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/Protocol.kt @@ -46,7 +46,7 @@ interface Protocol { /** * Generates a function signature like the following: * ```rust - * fn parse_http_error_metadata(response: &Response) -> 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 diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/RestJson.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/RestJson.kt index 09dba2bf1..675a0b212 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/RestJson.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/RestJson.kt @@ -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, diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/RestXml.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/RestXml.kt index c162b87c6..3045cf026 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/RestXml.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/RestXml.kt @@ -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) } } diff --git a/rust-runtime/aws-smithy-http/src/result.rs b/rust-runtime/aws-smithy-http/src/result.rs index 6cc5e5cdd..0ce42c7cb 100644 --- a/rust-runtime/aws-smithy-http/src/result.rs +++ b/rust-runtime/aws-smithy-http/src/result.rs @@ -433,6 +433,21 @@ impl SdkError { ServiceError(context) => Ok(context.source.into()), } } + + /// Maps the service error type in `SdkError::ServiceError` + #[doc(hidden)] + pub fn map_service_error(self, map: impl FnOnce(E) -> E2) -> SdkError { + match self { + Self::ServiceError(context) => SdkError::::ServiceError(ServiceError { + source: map(context.source), + raw: context.raw, + }), + Self::ConstructionFailure(context) => SdkError::::ConstructionFailure(context), + Self::DispatchFailure(context) => SdkError::::DispatchFailure(context), + Self::ResponseError(context) => SdkError::::ResponseError(context), + Self::TimeoutError(context) => SdkError::::TimeoutError(context), + } + } } impl Display for SdkError { diff --git a/rust-runtime/aws-smithy-runtime-api/Cargo.toml b/rust-runtime/aws-smithy-runtime-api/Cargo.toml index 1de8a0e60..9c6d96f52 100644 --- a/rust-runtime/aws-smithy-runtime-api/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime-api/Cargo.toml @@ -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 diff --git a/rust-runtime/aws-smithy-runtime-api/external-types.toml b/rust-runtime/aws-smithy-runtime-api/external-types.toml index 8a2e2ce81..4c9c93b25 100644 --- a/rust-runtime/aws-smithy-runtime-api/external-types.toml +++ b/rust-runtime/aws-smithy-runtime-api/external-types.toml @@ -1,3 +1,7 @@ allowed_external_types = [ "aws_smithy_types::*", + "aws_smithy_http::*", + + "http::request::Request", + "http::response::Response", ] diff --git a/rust-runtime/aws-smithy-runtime-api/src/client.rs b/rust-runtime/aws-smithy-runtime-api/src/client.rs new file mode 100644 index 000000000..3b01dca56 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/src/client.rs @@ -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; diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs b/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs new file mode 100644 index 000000000..f958fab91 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs @@ -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, + expiration: Option, +} + +impl Identity { + pub fn new(data: impl Any + Send + Sync, expiration: Option) -> Self { + Self { + data: Arc::new(data), + expiration, + } + } + + pub fn data(&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) {} + 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::().unwrap().first); + assert_eq!("bar", identity.data::().unwrap().last); + assert_eq!(Some(&expiration), identity.expiration()); + } +} diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs new file mode 100644 index 000000000..f584c3752 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs @@ -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, + 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 { + 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 { + client_interceptors: Vec>>, + operation_interceptors: Vec>>, +} + +impl Default for Interceptors { + 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, + 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, + cfg: &mut ConfigBag, + ) -> Result<(), InterceptorError> { + for interceptor in self.client_interceptors.iter_mut() { + interceptor.$inner_name(context, cfg)?; + } + Ok(()) + } + }; +} + +impl Interceptors { + pub fn new() -> Self { + Self::default() + } + + pub fn with_client_interceptor( + &mut self, + interceptor: impl Interceptor + 'static, + ) -> &mut Self { + self.client_interceptors.push(Box::new(interceptor)); + self + } + + pub fn with_operation_interceptor( + &mut self, + interceptor: impl Interceptor + '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); +} diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/interceptors/context.rs b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors/context.rs new file mode 100644 index 000000000..fc1065e05 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors/context.rs @@ -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; + +/// A container for the data currently available to an interceptor. +pub struct InterceptorContext { + input: Input, + output_or_error: Option, + request: Option, + response: Option, +} + +// 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 InterceptorContext { + 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, 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, 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) { + 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, + Option, + Option, + ) { + ( + self.input, + self.output_or_error, + self.request, + self.response, + ) + } +} diff --git a/rust-runtime/aws-smithy-runtime-api/src/interceptors/error.rs b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors/error.rs similarity index 95% rename from rust-runtime/aws-smithy-runtime-api/src/interceptors/error.rs rename to rust-runtime/aws-smithy-runtime-api/src/client/interceptors/error.rs index 024b4980f..9fba28092 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/interceptors/error.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors/error.rs @@ -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" diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs b/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs new file mode 100644 index 000000000..c70bcb8c1 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs @@ -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; +pub type HttpResponse = http::Response; +pub type BoxError = Box; +pub type BoxFallibleFut = Pin>>>; + +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; +} + +pub trait ResponseDeserializer: Send + Sync + Debug { + fn deserialize_streaming(&self, response: &mut HttpResponse) -> Option { + 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; +} + +pub trait RetryStrategy: Send + Sync + Debug { + fn should_attempt_initial_request(&self, cfg: &ConfigBag) -> Result<(), BoxError>; + + fn should_attempt_retry( + &self, + context: &InterceptorContext, + cfg: &ConfigBag, + ) -> Result; +} + +#[derive(Debug)] +pub struct AuthOptionResolverParams(TypeErasedBox); + +impl AuthOptionResolverParams { + pub fn new(params: T) -> Self { + Self(TypedBox::new(params).erase()) + } + + pub fn get(&self) -> Option<&T> { + self.0.downcast_ref() + } +} + +pub trait AuthOptionResolver: Send + Sync + Debug { + fn resolve_auth_options( + &self, + params: &AuthOptionResolverParams, + ) -> Result, BoxError>; +} + +#[derive(Clone, Debug)] +pub struct HttpAuthOption { + scheme_id: &'static str, + properties: Arc, +} + +impl HttpAuthOption { + pub fn new(scheme_id: &'static str, properties: Arc) -> 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; +} + +#[derive(Debug)] +pub struct IdentityResolvers { + identity_resolvers: Vec<(&'static str, Box)>, +} + +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)>, +} +#[derive(Debug)] +pub struct HttpAuthSchemes { + inner: Arc, +} + +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; +} + +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::() + .expect("auth option resolver params must be set") + } + + fn set_auth_option_resolver_params( + &mut self, + auth_option_resolver_params: AuthOptionResolverParams, + ) { + self.put::(auth_option_resolver_params); + } + + fn auth_option_resolver(&self) -> &dyn AuthOptionResolver { + &**self + .get::>() + .expect("an auth option resolver must be set") + } + + fn set_auth_option_resolver( + &mut self, + auth_option_resolver: impl AuthOptionResolver + 'static, + ) { + self.put::>(Box::new(auth_option_resolver)); + } + + fn http_auth_schemes(&self) -> &HttpAuthSchemes { + self.get::() + .expect("auth schemes must be set") + } + + fn set_http_auth_schemes(&mut self, http_auth_schemes: HttpAuthSchemes) { + self.put::(http_auth_schemes); + } + + fn retry_strategy(&self) -> &dyn RetryStrategy { + &**self + .get::>() + .expect("a retry strategy must be set") + } + + fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static) { + self.put::>(Box::new(retry_strategy)); + } + + fn endpoint_resolver(&self) -> &dyn EndpointResolver { + &**self + .get::>() + .expect("an endpoint resolver must be set") + } + + fn set_endpoint_resolver(&mut self, endpoint_resolver: impl EndpointResolver + 'static) { + self.put::>(Box::new(endpoint_resolver)); + } + + fn identity_resolvers(&self) -> &IdentityResolvers { + self.get::() + .expect("identity resolvers must be configured") + } + + fn set_identity_resolvers(&mut self, identity_resolvers: IdentityResolvers) { + self.put::(identity_resolvers); + } + + fn connection(&self) -> &dyn Connection { + &**self + .get::>() + .expect("missing connector") + } + + fn set_connection(&mut self, connection: impl Connection + 'static) { + self.put::>(Box::new(connection)); + } + + fn request_serializer(&self) -> &dyn RequestSerializer { + &**self + .get::>() + .expect("missing request serializer") + } + + fn set_request_serializer(&mut self, request_serializer: impl RequestSerializer + 'static) { + self.put::>(Box::new(request_serializer)); + } + + fn response_deserializer(&self) -> &dyn ResponseDeserializer { + &**self + .get::>() + .expect("missing response deserializer") + } + + fn set_response_deserializer( + &mut self, + response_deserializer: impl ResponseDeserializer + 'static, + ) { + self.put::>(Box::new(response_deserializer)); + } + + fn trace_probe(&self) -> &dyn TraceProbe { + &**self + .get::>() + .expect("missing trace probe") + } + + fn set_trace_probe(&mut self, trace_probe: impl TraceProbe + 'static) { + self.put::>(Box::new(trace_probe)); + } +} + +pub mod builders { + use super::*; + + #[derive(Debug, Default)] + pub struct IdentityResolversBuilder { + identity_resolvers: Vec<(&'static str, Box)>, + } + + 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)>, + } + + 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, + }), + } + } + } +} diff --git a/rust-runtime/aws-smithy-runtime-api/src/retries.rs b/rust-runtime/aws-smithy-runtime-api/src/client/retries.rs similarity index 100% rename from rust-runtime/aws-smithy-runtime-api/src/retries.rs rename to rust-runtime/aws-smithy-runtime-api/src/client/retries.rs diff --git a/rust-runtime/aws-smithy-runtime-api/src/retries/rate_limiting.rs b/rust-runtime/aws-smithy-runtime-api/src/client/retries/rate_limiting.rs similarity index 100% rename from rust-runtime/aws-smithy-runtime-api/src/retries/rate_limiting.rs rename to rust-runtime/aws-smithy-runtime-api/src/client/retries/rate_limiting.rs diff --git a/rust-runtime/aws-smithy-runtime-api/src/retries/rate_limiting/error.rs b/rust-runtime/aws-smithy-runtime-api/src/client/retries/rate_limiting/error.rs similarity index 100% rename from rust-runtime/aws-smithy-runtime-api/src/retries/rate_limiting/error.rs rename to rust-runtime/aws-smithy-runtime-api/src/client/retries/rate_limiting/error.rs diff --git a/rust-runtime/aws-smithy-runtime-api/src/retries/rate_limiting/token.rs b/rust-runtime/aws-smithy-runtime-api/src/client/retries/rate_limiting/token.rs similarity index 90% rename from rust-runtime/aws-smithy-runtime-api/src/retries/rate_limiting/token.rs rename to rust-runtime/aws-smithy-runtime-api/src/client/retries/rate_limiting/token.rs index 92cb565d8..70d620e79 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/retries/rate_limiting/token.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/retries/rate_limiting/token.rs @@ -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> = + let _tb: Box> = Box::new(TokenBucket::builder().build()); } } diff --git a/rust-runtime/aws-smithy-runtime-api/src/retries/rate_limiting/token_bucket.rs b/rust-runtime/aws-smithy-runtime-api/src/client/retries/rate_limiting/token_bucket.rs similarity index 100% rename from rust-runtime/aws-smithy-runtime-api/src/retries/rate_limiting/token_bucket.rs rename to rust-runtime/aws-smithy-runtime-api/src/client/retries/rate_limiting/token_bucket.rs diff --git a/rust-runtime/aws-smithy-runtime-api/src/runtime_plugin.rs b/rust-runtime/aws-smithy-runtime-api/src/client/runtime_plugin.rs similarity index 100% rename from rust-runtime/aws-smithy-runtime-api/src/runtime_plugin.rs rename to rust-runtime/aws-smithy-runtime-api/src/client/runtime_plugin.rs diff --git a/rust-runtime/aws-smithy-runtime-api/src/interceptors.rs b/rust-runtime/aws-smithy-runtime-api/src/interceptors.rs deleted file mode 100644 index 5d4eff539..000000000 --- a/rust-runtime/aws-smithy-runtime-api/src/interceptors.rs +++ /dev/null @@ -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 { - /// 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - cfg: &mut ConfigBag, - ) -> Result<(), InterceptorError> { - let _ctx = context; - let _cfg = cfg; - Ok(()) - } -} - -pub struct Interceptors { - client_interceptors: Vec>>, - operation_interceptors: Vec>>, -} - -impl Default for Interceptors { - fn default() -> Self { - Self { - client_interceptors: Vec::new(), - operation_interceptors: Vec::new(), - } - } -} - -impl Interceptors { - pub fn new() -> Self { - Self::default() - } - - pub fn with_client_interceptor( - &mut self, - interceptor: impl Interceptor + 'static, - ) -> &mut Self { - self.client_interceptors.push(Box::new(interceptor)); - self - } - - pub fn with_operation_interceptor( - &mut self, - interceptor: impl Interceptor + 'static, - ) -> &mut Self { - self.operation_interceptors.push(Box::new(interceptor)); - self - } - - fn all_interceptors_mut( - &mut self, - ) -> impl Iterator>> { - self.client_interceptors - .iter_mut() - .chain(self.operation_interceptors.iter_mut()) - } - - pub fn client_read_before_execution( - &mut self, - context: &InterceptorContext, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - cfg: &mut ConfigBag, - ) -> Result<(), InterceptorError> { - for interceptor in self.all_interceptors_mut() { - interceptor.read_after_execution(context, cfg)?; - } - Ok(()) - } -} diff --git a/rust-runtime/aws-smithy-runtime-api/src/interceptors/context.rs b/rust-runtime/aws-smithy-runtime-api/src/interceptors/context.rs deleted file mode 100644 index 64fb781b3..000000000 --- a/rust-runtime/aws-smithy-runtime-api/src/interceptors/context.rs +++ /dev/null @@ -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 { - modeled_request: ModReq, - tx_request: Option, - modeled_response: Option, - tx_response: Option, -} - -// 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 InterceptorContext { - 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)) - } -} diff --git a/rust-runtime/aws-smithy-runtime-api/src/lib.rs b/rust-runtime/aws-smithy-runtime-api/src/lib.rs index 0ccdd53f0..275f31175 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/lib.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/lib.rs @@ -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; diff --git a/rust-runtime/aws-smithy-runtime-api/src/type_erasure.rs b/rust-runtime/aws-smithy-runtime-api/src/type_erasure.rs new file mode 100644 index 000000000..72de2ccbb --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/src/type_erasure.rs @@ -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` 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 { + inner: TypeErasedBox, + _phantom: PhantomData, +} + +impl TypedBox +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` from a `TypeErasedBox`. + // + // If the `TypedBox` 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, TypeErasedBox> { + if type_erased.downcast_ref::().is_some() { + Ok(TypedBox { + inner: type_erased, + _phantom: Default::default(), + }) + } else { + Err(type_erased) + } + } + + /// Converts the `TypedBox` back into `T`. + pub fn unwrap(self) -> T { + *self.inner.downcast::().expect("type checked") + } + + /// Converts the `TypedBox` into a `TypeErasedBox`. + pub fn erase(self) -> TypeErasedBox { + self.inner + } +} + +impl Deref for TypedBox { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner.downcast_ref().expect("type checked") + } +} + +impl DerefMut for TypedBox { + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner.downcast_mut().expect("type checked") + } +} + +/// A new-type around `Box` +#[derive(Debug)] +pub struct TypeErasedBox { + inner: Box, +} + +impl TypeErasedBox { + // Creates a new `TypeErasedBox`. + pub fn new(inner: Box) -> Self { + Self { inner } + } + + // Downcast into a `Box`, or return `Self` if it is not a `T`. + pub fn downcast(self) -> Result, 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(&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(&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::() + .expect("I know its a Foo") + .0 = "3"; + + let bar_erased = bar.erase(); + + let bar_erased = TypedBox::::assume_from(bar_erased).expect_err("it's not a Foo"); + let mut bar = TypedBox::::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::().is_none()); + assert!(foo_erased.downcast_mut::().is_none()); + let mut foo_erased = foo_erased.downcast::().expect_err("it's not a Bar"); + + assert_eq!("3", foo_erased.downcast_ref::().expect("it's a Foo").0); + foo_erased.downcast_mut::().expect("it's a Foo").0 = "4"; + let foo = *foo_erased.downcast::().expect("it's a Foo"); + assert_eq!("4", foo.0); + } +} diff --git a/rust-runtime/aws-smithy-runtime/Cargo.toml b/rust-runtime/aws-smithy-runtime/Cargo.toml index cf0912728..4c035d154 100644 --- a/rust-runtime/aws-smithy-runtime/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime/Cargo.toml @@ -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 diff --git a/rust-runtime/aws-smithy-runtime/src/client.rs b/rust-runtime/aws-smithy-runtime/src/client.rs new file mode 100644 index 000000000..5870bd2af --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client.rs @@ -0,0 +1,6 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +pub mod orchestrator; diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs new file mode 100644 index 000000000..cdecbcc15 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs @@ -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, + runtime_plugins: &RuntimePlugins, + cfg: &mut ConfigBag, +) -> Result> { + 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, +) -> Result> { + 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)) +} diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs new file mode 100644 index 000000000..8f15051a9 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs @@ -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> { + 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()) + }) +} diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/http.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/http.rs new file mode 100644 index 000000000..247464a7b --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/http.rs @@ -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::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<(), ::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(()) +} diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/phase.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/phase.rs new file mode 100644 index 000000000..d163b5c74 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/phase.rs @@ -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, +} + +impl Phase { + pub(crate) fn construction(context: InterceptorContext) -> Self { + Self::start(OrchestrationPhase::Construction, context) + } + pub(crate) fn dispatch(context: InterceptorContext) -> Self { + Self::start(OrchestrationPhase::Dispatch, context) + } + pub(crate) fn response_handling( + context: InterceptorContext, + ) -> Self { + Self::start(OrchestrationPhase::ResponseHandling, context) + } + + fn start( + phase: OrchestrationPhase, + context: InterceptorContext, + ) -> 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>( + mut self, + c: impl FnOnce(&mut InterceptorContext) -> Result<(), E>, + ) -> Result> { + match c(&mut self.context) { + Ok(_) => Ok(self), + Err(e) => Err(self.fail(e)), + } + } + + pub(crate) fn include>( + self, + c: impl FnOnce(&InterceptorContext) -> Result<(), E>, + ) -> Result> { + match c(&self.context) { + Ok(_) => Ok(self), + Err(e) => Err(self.fail(e)), + } + } + + pub(crate) fn fail(self, e: impl Into) -> SdkError { + self.into_sdk_error(e.into()) + } + + pub(crate) fn finalize(self) -> Result> { + 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 { + let e = match e.downcast::() { + 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 { + self.context + } +} diff --git a/rust-runtime/aws-smithy-runtime/src/lib.rs b/rust-runtime/aws-smithy-runtime/src/lib.rs index b4d85b5ed..195fde2d0 100644 --- a/rust-runtime/aws-smithy-runtime/src/lib.rs +++ b/rust-runtime/aws-smithy-runtime/src/lib.rs @@ -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; -pub type BoxFallibleFut = Pin>>>; - -pub trait TraceProbe: Send + Sync + Debug { - fn dispatch_events(&self, cfg: &ConfigBag) -> BoxFallibleFut<()>; -} - -pub trait RequestSerializer: Send + Sync + Debug { - fn serialize_request(&self, req: &mut In, cfg: &ConfigBag) -> Result; -} - -pub trait ResponseDeserializer: Send + Sync + Debug { - fn deserialize_response(&self, res: &mut TxRes, cfg: &ConfigBag) -> Result; -} - -pub trait Connection: Send + Sync + Debug { - fn call(&self, req: &mut TxReq, cfg: &ConfigBag) -> BoxFallibleFut; -} - -pub trait RetryStrategy: Send + Sync + Debug { - fn should_retry(&self, res: &Out, cfg: &ConfigBag) -> Result; -} - -pub trait AuthOrchestrator: Send + Sync + Debug { - fn auth_request(&self, req: &mut Req, cfg: &ConfigBag) -> Result<(), BoxError>; -} - -pub trait EndpointOrchestrator: 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, BoxError>; -} - -/// `In`: The input message e.g. `ListObjectsRequest` -/// `Req`: The transport request message e.g. `http::Request` -/// `Res`: The transport response message e.g. `http::Response` -/// `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( - input: In, - interceptors: &mut Interceptors>, - runtime_plugins: &RuntimePlugins, - cfg: &mut ConfigBag, -) -> Result -where - // The input must be Clone in case of retries - In: Clone + 'static, - Req: 'static, - Res: 'static, - T: 'static, -{ - let mut ctx: InterceptorContext> = - 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::>>() - .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::>>>() - .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::>() - .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( - ctx: &mut InterceptorContext>, - cfg: &mut ConfigBag, - interceptors: &mut Interceptors>, -) -> 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::>>() - .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::>>() - .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::>>() - .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::>>>() - .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; diff --git a/rust-runtime/inlineable/src/json_errors.rs b/rust-runtime/inlineable/src/json_errors.rs index 1973f59d7..c2669a795 100644 --- a/rust-runtime/inlineable/src/json_errors.rs +++ b/rust-runtime/inlineable/src/json_errors.rs @@ -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) -> Result, ) -> Result { - 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) diff --git a/settings.gradle.kts b/settings.gradle.kts index 48190cc2a..88f55a4e9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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 -- GitLab