diff --git a/aws/rust-runtime/aws-types/src/sdk_config.rs b/aws/rust-runtime/aws-types/src/sdk_config.rs index 3c6c69b612..f3a0301ae8 100644 --- a/aws/rust-runtime/aws-types/src/sdk_config.rs +++ b/aws/rust-runtime/aws-types/src/sdk_config.rs @@ -442,7 +442,7 @@ impl Builder { /// use std::time::Duration; /// use aws_smithy_client::hyper_ext; /// use aws_smithy_client::http_connector::ConnectorSettings; - /// use aws_types::sdk_config::{SdkConfig, Builder}; + /// use aws_types::sdk_config::{Builder, SdkConfig}; /// /// fn override_http_connector(builder: &mut Builder) { /// let https_connector = hyper_rustls::HttpsConnectorBuilder::new() diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt index 1d94cd6082..f5d8afcbe2 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt @@ -77,7 +77,6 @@ class GenericSmithySdkConfigSettings : ClientCodegenDecorator { ${section.serviceConfigBuilder}.set_sleep_impl(${section.sdkConfig}.sleep_impl()); ${section.serviceConfigBuilder}.set_http_connector(${section.sdkConfig}.http_connector().cloned()); - """, ) }, diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/tests/interceptors.rs b/aws/sra-test/integration-tests/aws-sdk-s3/tests/interceptors.rs new file mode 100644 index 0000000000..208bd8aa8b --- /dev/null +++ b/aws/sra-test/integration-tests/aws-sdk-s3/tests/interceptors.rs @@ -0,0 +1,120 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +mod util; + +use aws_sdk_s3::config::{Credentials, Region}; +use aws_sdk_s3::Client; +use aws_smithy_client::dvr; +use aws_smithy_client::dvr::MediaType; +use aws_smithy_client::erase::DynConnector; +use aws_smithy_runtime_api::client::interceptors::context::phase::BeforeTransmit; +use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext}; +use aws_smithy_runtime_api::client::orchestrator::ConfigBagAccessors; +use aws_smithy_runtime_api::client::orchestrator::RequestTime; +use aws_smithy_runtime_api::config_bag::ConfigBag; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +const LIST_BUCKETS_PATH: &str = "test-data/list-objects-v2.json"; + +#[tokio::test] +async fn operation_interceptor_test() { + tracing_subscriber::fmt::init(); + + let conn = dvr::ReplayingConnection::from_file(LIST_BUCKETS_PATH).unwrap(); + + // Not setting `TestUserAgentInterceptor` here, expecting it to be set later by the + // operation-level config. + let config = aws_sdk_s3::Config::builder() + .credentials_provider(Credentials::for_tests()) + .region(Region::new("us-east-1")) + .http_connector(DynConnector::new(conn.clone())) + .build(); + let client = Client::from_conf(config); + let fixup = util::FixupPlugin { + timestamp: UNIX_EPOCH + Duration::from_secs(1624036048), + }; + + let resp = dbg!( + client + .list_objects_v2() + .config_override( + aws_sdk_s3::Config::builder().interceptor(util::TestUserAgentInterceptor) + ) + .bucket("test-bucket") + .prefix("prefix~") + .send_orchestrator_with_plugin(Some(fixup)) + .await + ); + let resp = resp.expect("valid e2e test"); + assert_eq!(resp.name(), Some("test-bucket")); + conn.full_validate(MediaType::Xml).await.expect("success") +} + +#[derive(Debug)] +struct RequestTimeResetInterceptor; +impl Interceptor for RequestTimeResetInterceptor { + fn modify_before_signing( + &self, + _context: &mut InterceptorContext, + cfg: &mut ConfigBag, + ) -> Result<(), aws_smithy_runtime_api::client::interceptors::BoxError> { + cfg.set_request_time(RequestTime::new(UNIX_EPOCH)); + + Ok(()) + } +} + +#[derive(Debug)] +struct RequestTimeAdvanceInterceptor(Duration); +impl Interceptor for RequestTimeAdvanceInterceptor { + fn modify_before_signing( + &self, + _context: &mut InterceptorContext, + cfg: &mut ConfigBag, + ) -> Result<(), aws_smithy_runtime_api::client::interceptors::BoxError> { + let request_time = cfg.request_time().unwrap(); + let request_time = RequestTime::new(request_time.system_time() + self.0); + cfg.set_request_time(request_time); + + Ok(()) + } +} + +#[tokio::test] +async fn interceptor_priority() { + let conn = dvr::ReplayingConnection::from_file(LIST_BUCKETS_PATH).unwrap(); + + // `RequestTimeResetInterceptor` will reset a `RequestTime` to `UNIX_EPOCH`, whose previous + // value should be `SystemTime::now()` set by `FixupPlugin`. + let config = aws_sdk_s3::Config::builder() + .credentials_provider(Credentials::for_tests()) + .region(Region::new("us-east-1")) + .http_connector(DynConnector::new(conn.clone())) + .interceptor(util::TestUserAgentInterceptor) + .interceptor(RequestTimeResetInterceptor) + .build(); + let client = Client::from_conf(config); + let fixup = util::FixupPlugin { + timestamp: SystemTime::now(), + }; + + // `RequestTimeAdvanceInterceptor` configured at the operation level should run after, + // expecting the `RequestTime` to move forward by the specified amount since `UNIX_EPOCH`. + let resp = dbg!( + client + .list_objects_v2() + .config_override(aws_sdk_s3::Config::builder().interceptor( + RequestTimeAdvanceInterceptor(Duration::from_secs(1624036048)) + )) + .bucket("test-bucket") + .prefix("prefix~") + .send_orchestrator_with_plugin(Some(fixup)) + .await + ); + let resp = resp.expect("valid e2e test"); + assert_eq!(resp.name(), Some("test-bucket")); + conn.full_validate(MediaType::Xml).await.expect("success") +} diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs b/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs index 8ff073d021..69f89d1a9d 100644 --- a/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs +++ b/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs @@ -3,18 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_http::user_agent::AwsUserAgent; -use aws_runtime::invocation_id::InvocationId; +mod util; + use aws_sdk_s3::config::{Credentials, Region}; use aws_sdk_s3::Client; use aws_smithy_client::dvr; use aws_smithy_client::dvr::MediaType; use aws_smithy_client::erase::DynConnector; -use aws_smithy_runtime_api::client::interceptors::Interceptors; -use aws_smithy_runtime_api::client::orchestrator::{ConfigBagAccessors, RequestTime}; -use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin; -use aws_smithy_runtime_api::config_bag::ConfigBag; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, UNIX_EPOCH}; const LIST_BUCKETS_PATH: &str = "test-data/list-objects-v2.json"; @@ -28,16 +24,16 @@ async fn sra_test() { .credentials_provider(Credentials::for_tests()) .region(Region::new("us-east-1")) .http_connector(DynConnector::new(conn.clone())) + .interceptor(util::TestUserAgentInterceptor) .build(); let client = Client::from_conf(config); - let fixup = FixupPlugin { + let fixup = util::FixupPlugin { timestamp: UNIX_EPOCH + Duration::from_secs(1624036048), }; let resp = dbg!( client .list_objects_v2() - .config_override(aws_sdk_s3::Config::builder().force_path_style(false)) .bucket("test-bucket") .prefix("prefix~") .send_orchestrator_with_plugin(Some(fixup)) @@ -47,22 +43,5 @@ async fn sra_test() { // conn.dump_to_file("test-data/list-objects-v2.json").unwrap(); let resp = resp.expect("valid e2e test"); assert_eq!(resp.name(), Some("test-bucket")); - conn.full_validate(MediaType::Xml).await.expect("failed") -} - -#[derive(Debug)] -struct FixupPlugin { - timestamp: SystemTime, -} -impl RuntimePlugin for FixupPlugin { - fn configure( - &self, - cfg: &mut ConfigBag, - _interceptors: &mut Interceptors, - ) -> Result<(), aws_smithy_runtime_api::client::runtime_plugin::BoxError> { - cfg.set_request_time(RequestTime::new(self.timestamp.clone())); - cfg.put(AwsUserAgent::for_tests()); - cfg.put(InvocationId::for_tests()); - Ok(()) - } + conn.full_validate(MediaType::Xml).await.expect("success") } diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/tests/util.rs b/aws/sra-test/integration-tests/aws-sdk-s3/tests/util.rs new file mode 100644 index 0000000000..af3d53094f --- /dev/null +++ b/aws/sra-test/integration-tests/aws-sdk-s3/tests/util.rs @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_http::user_agent::AwsUserAgent; +use aws_runtime::invocation_id::InvocationId; +use aws_smithy_runtime_api::client::interceptors::context::phase::BeforeTransmit; +use aws_smithy_runtime_api::client::interceptors::{ + Interceptor, InterceptorContext, InterceptorRegistrar, +}; +use aws_smithy_runtime_api::client::orchestrator::{ConfigBagAccessors, RequestTime}; +use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin; +use aws_smithy_runtime_api::config_bag::ConfigBag; +use http::header::USER_AGENT; +use http::{HeaderName, HeaderValue}; +use std::time::SystemTime; + +pub const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent"); + +#[derive(Debug)] +pub struct FixupPlugin { + pub timestamp: SystemTime, +} +impl RuntimePlugin for FixupPlugin { + fn configure( + &self, + cfg: &mut ConfigBag, + _interceptors: &mut InterceptorRegistrar, + ) -> Result<(), aws_smithy_runtime_api::client::runtime_plugin::BoxError> { + cfg.set_request_time(RequestTime::new(self.timestamp.clone())); + cfg.put(InvocationId::for_tests()); + Ok(()) + } +} + +#[derive(Debug)] +pub struct TestUserAgentInterceptor; +impl Interceptor for TestUserAgentInterceptor { + fn modify_before_signing( + &self, + context: &mut InterceptorContext, + _cfg: &mut ConfigBag, + ) -> Result<(), aws_smithy_runtime_api::client::interceptors::BoxError> { + let headers = context.request_mut().headers_mut(); + let user_agent = AwsUserAgent::for_tests(); + // Overwrite user agent header values provided by `UserAgentInterceptor` + headers.insert(USER_AGENT, HeaderValue::try_from(user_agent.ua_header())?); + headers.insert( + X_AMZ_USER_AGENT, + HeaderValue::try_from(user_agent.aws_ua_header())?, + ); + + Ok(()) + } +} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpConnectorConfigDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpConnectorConfigDecorator.kt index d5ecd078b1..ba9c68f5f7 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpConnectorConfigDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpConnectorConfigDecorator.kt @@ -111,7 +111,6 @@ private class HttpConnectorConfigCustomization( /// use std::time::Duration; /// use aws_smithy_client::hyper_ext; /// use aws_smithy_client::http_connector::ConnectorSettings; - /// use crate::sdk_config::{SdkConfig, Builder}; /// use $moduleUseName::config::{Builder, Config}; /// /// fn override_http_connector(builder: &mut Builder) { diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/InterceptorConfigCustomization.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/InterceptorConfigCustomization.kt new file mode 100644 index 0000000000..664880b96b --- /dev/null +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/InterceptorConfigCustomization.kt @@ -0,0 +1,189 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.client.smithy.customizations + +import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +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 + +class InterceptorConfigCustomization(codegenContext: CodegenContext) : ConfigCustomization() { + private val moduleUseName = codegenContext.moduleUseName() + private val runtimeConfig = codegenContext.runtimeConfig + private val interceptors = RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("client::interceptors") + private val codegenScope = arrayOf( + "Interceptor" to interceptors.resolve("Interceptor"), + "SharedInterceptor" to interceptors.resolve("SharedInterceptor"), + ) + + override fun section(section: ServiceConfig) = + writable { + when (section) { + ServiceConfig.ConfigStruct -> rustTemplate( + """ + pub(crate) interceptors: Vec<#{SharedInterceptor}>, + """, + *codegenScope, + ) + + ServiceConfig.BuilderStruct -> + rustTemplate( + """ + interceptors: Vec<#{SharedInterceptor}>, + """, + *codegenScope, + ) + + ServiceConfig.ConfigImpl -> rustTemplate( + """ + // TODO(enableNewSmithyRuntime): Remove this doc hidden upon launch + ##[doc(hidden)] + /// Returns interceptors currently registered by the user. + pub fn interceptors(&self) -> impl Iterator + '_ { + self.interceptors.iter() + } + """, + *codegenScope, + ) + + ServiceConfig.BuilderImpl -> + rustTemplate( + """ + // TODO(enableNewSmithyRuntime): Remove this doc hidden upon launch + ##[doc(hidden)] + /// Add an [`Interceptor`](#{Interceptor}) that runs at specific stages of the request execution pipeline. + /// + /// Interceptors targeted at a certain stage are executed according to the pre-defined priority. + /// The SDK provides a default set of interceptors. An interceptor configured by this method + /// will run after those default interceptors. + /// + /// ## Examples + /// ```no_run + /// ## ##[cfg(test)] + /// ## mod tests { + /// ## ##[test] + /// ## fn example() { + /// use aws_smithy_runtime_api::client::interceptors::context::phase::BeforeTransmit; + /// use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext}; + /// use aws_smithy_runtime_api::config_bag::ConfigBag; + /// use $moduleUseName::config::Config; + /// + /// fn base_url() -> String { + /// // ... + /// ## String::new() + /// } + /// + /// ##[derive(Debug)] + /// pub struct UriModifierInterceptor; + /// impl Interceptor for UriModifierInterceptor { + /// fn modify_before_signing( + /// &self, + /// context: &mut InterceptorContext, + /// _cfg: &mut ConfigBag, + /// ) -> Result<(), aws_smithy_runtime_api::client::interceptors::BoxError> { + /// let request = context.request_mut(); + /// let uri = format!("{}{}", base_url(), request.uri().path()); + /// *request.uri_mut() = uri.parse()?; + /// + /// Ok(()) + /// } + /// } + /// + /// let config = Config::builder() + /// .interceptor(UriModifierInterceptor) + /// .build(); + /// ## } + /// ## } + /// ``` + pub fn interceptor(mut self, interceptor: impl #{Interceptor} + Send + Sync + 'static) -> Self { + self.add_interceptor(#{SharedInterceptor}::new(interceptor)); + self + } + + // TODO(enableNewSmithyRuntime): Remove this doc hidden upon launch + ##[doc(hidden)] + /// Add a [`SharedInterceptor`](#{SharedInterceptor}) that runs at specific stages of the request execution pipeline. + /// + /// Interceptors targeted at a certain stage are executed according to the pre-defined priority. + /// The SDK provides a default set of interceptors. An interceptor configured by this method + /// will run after those default interceptors. + /// + /// ## Examples + /// ```no_run + /// ## ##[cfg(test)] + /// ## mod tests { + /// ## ##[test] + /// ## fn example() { + /// use aws_smithy_runtime_api::client::interceptors::context::phase::BeforeTransmit; + /// use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext, SharedInterceptor}; + /// use aws_smithy_runtime_api::config_bag::ConfigBag; + /// use $moduleUseName::config::{Builder, Config}; + /// + /// fn base_url() -> String { + /// // ... + /// ## String::new() + /// } + /// + /// fn modify_request_uri(builder: &mut Builder) { + /// ##[derive(Debug)] + /// pub struct UriModifierInterceptor; + /// impl Interceptor for UriModifierInterceptor { + /// fn modify_before_signing( + /// &self, + /// context: &mut InterceptorContext, + /// _cfg: &mut ConfigBag, + /// ) -> Result<(), aws_smithy_runtime_api::client::interceptors::BoxError> { + /// let request = context.request_mut(); + /// let uri = format!("{}{}", base_url(), request.uri().path()); + /// *request.uri_mut() = uri.parse()?; + /// + /// Ok(()) + /// } + /// } + /// builder.add_interceptor(SharedInterceptor::new(UriModifierInterceptor)); + /// } + /// + /// let mut builder = Config::builder(); + /// modify_request_uri(&mut builder); + /// let config = builder.build(); + /// ## } + /// ## } + /// ``` + pub fn add_interceptor(&mut self, interceptor: #{SharedInterceptor}) -> &mut Self { + self.interceptors.push(interceptor); + self + } + + // TODO(enableNewSmithyRuntime): Remove this doc hidden upon launch + ##[doc(hidden)] + /// Set [`SharedInterceptor`](#{SharedInterceptor})s for the builder. + pub fn set_interceptors(&mut self, interceptors: impl IntoIterator) -> &mut Self { + self.interceptors = interceptors.into_iter().collect(); + self + } + """, + *codegenScope, + ) + + ServiceConfig.BuilderBuild -> rust( + """ + interceptors: self.interceptors, + """, + ) + + ServiceConfig.ToRuntimePlugin -> rust( + """ + interceptors.extend(self.interceptors.iter().cloned()); + """, + ) + + else -> emptySection + } + } +} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt index 42bcc6c368..d43c5706ec 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt @@ -12,6 +12,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.customizations.Endpoint import software.amazon.smithy.rust.codegen.client.smithy.customizations.HttpChecksumRequiredGenerator import software.amazon.smithy.rust.codegen.client.smithy.customizations.HttpVersionListCustomization import software.amazon.smithy.rust.codegen.client.smithy.customizations.IdempotencyTokenGenerator +import software.amazon.smithy.rust.codegen.client.smithy.customizations.InterceptorConfigCustomization import software.amazon.smithy.rust.codegen.client.smithy.customizations.ResiliencyConfigCustomization import software.amazon.smithy.rust.codegen.client.smithy.customizations.ResiliencyReExportCustomization import software.amazon.smithy.rust.codegen.client.smithy.customizations.ResiliencyServiceRuntimePluginCustomization @@ -52,7 +53,14 @@ class RequiredCustomizations : ClientCodegenDecorator { codegenContext: ClientCodegenContext, baseCustomizations: List, ): List = - baseCustomizations + ResiliencyConfigCustomization(codegenContext) + // TODO(enableNewSmithyRuntime): Keep only then branch once we switch to orchestrator + if (codegenContext.smithyRuntimeMode.generateOrchestrator) { + baseCustomizations + ResiliencyConfigCustomization(codegenContext) + InterceptorConfigCustomization( + codegenContext, + ) + } else { + baseCustomizations + ResiliencyConfigCustomization(codegenContext) + } override fun libRsCustomizations( codegenContext: ClientCodegenContext, diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt index 45da1913dc..ac07a1d4b7 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt @@ -23,19 +23,17 @@ sealed class OperationRuntimePluginSection(name: String) : Section(name) { */ data class AdditionalConfig( val configBagName: String, - val interceptorName: String, + val interceptorRegistrarName: String, val operationShape: OperationShape, ) : OperationRuntimePluginSection("AdditionalConfig") { fun registerInterceptor(runtimeConfig: RuntimeConfig, writer: RustWriter, interceptor: Writable) { val smithyRuntimeApi = RuntimeType.smithyRuntimeApi(runtimeConfig) writer.rustTemplate( """ - $interceptorName.register_operation_interceptor(std::sync::Arc::new(#{interceptor}) as _); + $interceptorRegistrarName.register(#{SharedInterceptor}::new(#{interceptor}) as _); """, - "HttpRequest" to smithyRuntimeApi.resolve("client::orchestrator::HttpRequest"), - "HttpResponse" to smithyRuntimeApi.resolve("client::orchestrator::HttpResponse"), - "Interceptors" to smithyRuntimeApi.resolve("client::interceptors::Interceptors"), "interceptor" to interceptor, + "SharedInterceptor" to smithyRuntimeApi.resolve("client::interceptors::SharedInterceptor"), ) } } @@ -75,14 +73,14 @@ class OperationRuntimePluginGenerator( private val codegenScope = codegenContext.runtimeConfig.let { rc -> val runtimeApi = RuntimeType.smithyRuntimeApi(rc) arrayOf( - "StaticAuthOptionResolverParams" to runtimeApi.resolve("client::auth::option_resolver::StaticAuthOptionResolverParams"), "AuthOptionResolverParams" to runtimeApi.resolve("client::auth::AuthOptionResolverParams"), "BoxError" to runtimeApi.resolve("client::runtime_plugin::BoxError"), "ConfigBag" to runtimeApi.resolve("config_bag::ConfigBag"), "ConfigBagAccessors" to runtimeApi.resolve("client::orchestrator::ConfigBagAccessors"), + "InterceptorRegistrar" to runtimeApi.resolve("client::interceptors::InterceptorRegistrar"), "RetryClassifiers" to runtimeApi.resolve("client::retries::RetryClassifiers"), "RuntimePlugin" to runtimeApi.resolve("client::runtime_plugin::RuntimePlugin"), - "Interceptors" to runtimeApi.resolve("client::interceptors::Interceptors"), + "StaticAuthOptionResolverParams" to runtimeApi.resolve("client::auth::option_resolver::StaticAuthOptionResolverParams"), ) } @@ -95,7 +93,7 @@ class OperationRuntimePluginGenerator( writer.rustTemplate( """ impl #{RuntimePlugin} for $operationStructName { - fn configure(&self, cfg: &mut #{ConfigBag}, _interceptors: &mut #{Interceptors}) -> Result<(), #{BoxError}> { + fn configure(&self, cfg: &mut #{ConfigBag}, _interceptors: &mut #{InterceptorRegistrar}) -> Result<(), #{BoxError}> { use #{ConfigBagAccessors} as _; cfg.set_request_serializer(${operationStructName}RequestSerializer); cfg.set_response_deserializer(${operationStructName}ResponseDeserializer); diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt index a5a0edca2c..d00af9b66b 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt @@ -33,7 +33,7 @@ sealed class ServiceRuntimePluginSection(name: String) : Section(name) { /** * Hook for adding additional things to config inside service runtime plugins. */ - data class AdditionalConfig(val configBagName: String, val interceptorName: String) : ServiceRuntimePluginSection("AdditionalConfig") { + data class AdditionalConfig(val configBagName: String, val interceptorRegistrarName: String) : ServiceRuntimePluginSection("AdditionalConfig") { /** Adds a value to the config bag */ fun putConfigValue(writer: RustWriter, value: Writable) { writer.rust("$configBagName.put(#T);", value) @@ -44,12 +44,10 @@ sealed class ServiceRuntimePluginSection(name: String) : Section(name) { val smithyRuntimeApi = RuntimeType.smithyRuntimeApi(runtimeConfig) writer.rustTemplate( """ - $interceptorName.register_client_interceptor(std::sync::Arc::new(#{interceptor}) as _); + $interceptorRegistrarName.register(#{SharedInterceptor}::new(#{interceptor}) as _); """, - "HttpRequest" to smithyRuntimeApi.resolve("client::orchestrator::HttpRequest"), - "HttpResponse" to smithyRuntimeApi.resolve("client::orchestrator::HttpResponse"), - "Interceptors" to smithyRuntimeApi.resolve("client::interceptors::Interceptors"), "interceptor" to interceptor, + "SharedInterceptor" to smithyRuntimeApi.resolve("client::interceptors::SharedInterceptor"), ) } } @@ -80,11 +78,11 @@ class ServiceRuntimePluginGenerator( "DynConnectorAdapter" to runtime.resolve("client::connections::adapter::DynConnectorAdapter"), "HttpAuthSchemes" to runtimeApi.resolve("client::auth::HttpAuthSchemes"), "IdentityResolvers" to runtimeApi.resolve("client::identity::IdentityResolvers"), + "InterceptorRegistrar" to runtimeApi.resolve("client::interceptors::InterceptorRegistrar"), "NeverRetryStrategy" to runtime.resolve("client::retries::strategy::NeverRetryStrategy"), "Params" to endpointTypesGenerator.paramsStruct(), "ResolveEndpoint" to http.resolve("endpoint::ResolveEndpoint"), "RuntimePlugin" to runtimeApi.resolve("client::runtime_plugin::RuntimePlugin"), - "Interceptors" to runtimeApi.resolve("client::interceptors::Interceptors"), "SharedEndpointResolver" to http.resolve("endpoint::SharedEndpointResolver"), "StaticAuthOptionResolver" to runtimeApi.resolve("client::auth::option_resolver::StaticAuthOptionResolver"), ) @@ -105,7 +103,7 @@ class ServiceRuntimePluginGenerator( } impl #{RuntimePlugin} for ServiceRuntimePlugin { - fn configure(&self, cfg: &mut #{ConfigBag}, _interceptors: &mut #{Interceptors}) -> #{Result}<(), #{BoxError}> { + fn configure(&self, cfg: &mut #{ConfigBag}, _interceptors: &mut #{InterceptorRegistrar}) -> #{Result}<(), #{BoxError}> { use #{ConfigBagAccessors}; // HACK: Put the handle into the config bag to work around config not being fully implemented yet @@ -135,6 +133,10 @@ class ServiceRuntimePluginGenerator( cfg.set_connection(connection); #{additional_config} + + // Client-level Interceptors are registered after default Interceptors. + _interceptors.extend(self.handle.conf.interceptors.iter().cloned()); + Ok(()) } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/ServiceConfigGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/ServiceConfigGenerator.kt index 39ecaf164e..3112798fe0 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/ServiceConfigGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/ServiceConfigGenerator.kt @@ -87,6 +87,11 @@ sealed class ServiceConfig(name: String) : Section(name) { */ object BuilderBuild : ServiceConfig("BuilderBuild") + /** + * A section for setting up a field to be used by RuntimePlugin + */ + object ToRuntimePlugin : ServiceConfig("ToRuntimePlugin") + /** * A section for extra functionality that needs to be defined with the config module */ @@ -146,6 +151,8 @@ fun standardConfigParam(param: ConfigParam): ConfigCustomization = object : Conf rust("${param.name}: self.${param.name},") } + ServiceConfig.ToRuntimePlugin -> emptySection + else -> emptySection } } @@ -292,18 +299,22 @@ class ServiceConfigGenerator(private val customizations: List Result<(), #{BoxError}> { - // TODO(RuntimePlugins): Put into `cfg` the fields in `self.config_override` that are not `None`. - - Ok(()) - } + fn configure(&self, _cfg: &mut #{ConfigBag}, interceptors: &mut #{InterceptorRegistrar}) -> Result<(), #{BoxError}> """, "BoxError" to runtimeApi.resolve("client::runtime_plugin::BoxError"), "ConfigBag" to runtimeApi.resolve("config_bag::ConfigBag"), - "Interceptors" to runtimeApi.resolve("client::interceptors::Interceptors"), - ) + "InterceptorRegistrar" to runtimeApi.resolve("client::interceptors::InterceptorRegistrar"), + ) { + rust("// TODO(enableNewSmithyRuntime): Put into `cfg` the fields in `self.config_override` that are not `None`") + + customizations.forEach { + it.section(ServiceConfig.ToRuntimePlugin)(writer) + } + + rust("Ok(())") + } } } } diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs index f025cb5a70..ce4195d68c 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs @@ -13,6 +13,7 @@ use crate::config_bag::ConfigBag; use aws_smithy_types::error::display::DisplayErrorContext; pub use context::InterceptorContext; pub use error::{BoxError, InterceptorError}; +use std::ops::Deref; use std::sync::Arc; macro_rules! interceptor_trait_fn { @@ -563,12 +564,58 @@ pub trait Interceptor: std::fmt::Debug { ); } -pub type SharedInterceptor = Arc; +/// Interceptor wrapper that may be shared +#[derive(Debug, Clone)] +pub struct SharedInterceptor(Arc); + +impl SharedInterceptor { + /// Create a new `SharedInterceptor` from `Interceptor` + pub fn new(interceptor: impl Interceptor + Send + Sync + 'static) -> Self { + Self(Arc::new(interceptor)) + } +} + +impl AsRef for SharedInterceptor { + fn as_ref(&self) -> &(dyn Interceptor + 'static) { + self.0.as_ref() + } +} + +impl From> for SharedInterceptor { + fn from(interceptor: Arc) -> Self { + SharedInterceptor(interceptor) + } +} + +impl Deref for SharedInterceptor { + type Target = Arc; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// Collection of [`SharedInterceptor`] that allows for only registration +#[derive(Debug, Clone, Default)] +pub struct InterceptorRegistrar(Vec); + +impl InterceptorRegistrar { + pub fn register(&mut self, interceptor: SharedInterceptor) { + self.0.push(interceptor); + } +} + +impl Extend for InterceptorRegistrar { + fn extend>(&mut self, iter: T) { + for interceptor in iter { + self.register(interceptor); + } + } +} #[derive(Debug, Clone, Default)] pub struct Interceptors { - client_interceptors: Vec, - operation_interceptors: Vec, + client_interceptors: InterceptorRegistrar, + operation_interceptors: InterceptorRegistrar, } macro_rules! interceptor_impl_fn { @@ -619,18 +666,17 @@ impl Interceptors { // Since interceptors can modify the interceptor list (since its in the config bag), copy the list ahead of time. // This should be cheap since the interceptors inside the list are Arcs. self.client_interceptors + .0 .iter() - .chain(self.operation_interceptors.iter()) + .chain(self.operation_interceptors.0.iter()) } - pub fn register_client_interceptor(&mut self, interceptor: SharedInterceptor) -> &mut Self { - self.client_interceptors.push(interceptor); - self + pub fn client_interceptors_mut(&mut self) -> &mut InterceptorRegistrar { + &mut self.client_interceptors } - pub fn register_operation_interceptor(&mut self, interceptor: SharedInterceptor) -> &mut Self { - self.operation_interceptors.push(interceptor); - self + pub fn operation_interceptors_mut(&mut self) -> &mut InterceptorRegistrar { + &mut self.operation_interceptors } interceptor_impl_fn!( @@ -676,3 +722,28 @@ impl Interceptors { interceptor_impl_fn!(mut context, modify_before_completion, AfterDeserialization); interceptor_impl_fn!(context, read_after_execution, AfterDeserialization); } + +#[cfg(test)] +mod tests { + use crate::client::interceptors::{Interceptor, InterceptorRegistrar, SharedInterceptor}; + + #[derive(Debug)] + struct TestInterceptor; + impl Interceptor for TestInterceptor {} + + #[test] + fn register_interceptor() { + let mut registrar = InterceptorRegistrar::default(); + registrar.register(SharedInterceptor::new(TestInterceptor)); + assert_eq!(1, registrar.0.len()); + } + + #[test] + fn bulk_register_interceptors() { + let mut registrar = InterceptorRegistrar::default(); + let number_of_interceptors = 3; + let interceptors = vec![SharedInterceptor::new(TestInterceptor); number_of_interceptors]; + registrar.extend(interceptors); + assert_eq!(number_of_interceptors, registrar.0.len()); + } +} diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/runtime_plugin.rs b/rust-runtime/aws-smithy-runtime-api/src/client/runtime_plugin.rs index 63aa46ff69..ea3754cd06 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/runtime_plugin.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/runtime_plugin.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::client::interceptors::Interceptors; +use crate::client::interceptors::InterceptorRegistrar; use crate::config_bag::ConfigBag; use std::fmt::Debug; @@ -14,7 +14,7 @@ pub trait RuntimePlugin: Debug { fn configure( &self, cfg: &mut ConfigBag, - interceptors: &mut Interceptors, + interceptors: &mut InterceptorRegistrar, ) -> Result<(), BoxError>; } @@ -22,7 +22,7 @@ impl RuntimePlugin for BoxRuntimePlugin { fn configure( &self, cfg: &mut ConfigBag, - interceptors: &mut Interceptors, + interceptors: &mut InterceptorRegistrar, ) -> Result<(), BoxError> { self.as_ref().configure(cfg, interceptors) } @@ -58,7 +58,7 @@ impl RuntimePlugins { pub fn apply_client_configuration( &self, cfg: &mut ConfigBag, - interceptors: &mut Interceptors, + interceptors: &mut InterceptorRegistrar, ) -> Result<(), BoxError> { for plugin in self.client_plugins.iter() { plugin.configure(cfg, interceptors)?; @@ -70,7 +70,7 @@ impl RuntimePlugins { pub fn apply_operation_configuration( &self, cfg: &mut ConfigBag, - interceptors: &mut Interceptors, + interceptors: &mut InterceptorRegistrar, ) -> Result<(), BoxError> { for plugin in self.operation_plugins.iter() { plugin.configure(cfg, interceptors)?; @@ -83,7 +83,7 @@ impl RuntimePlugins { #[cfg(test)] mod tests { use super::{BoxError, RuntimePlugin, RuntimePlugins}; - use crate::client::interceptors::Interceptors; + use crate::client::interceptors::InterceptorRegistrar; use crate::config_bag::ConfigBag; #[derive(Debug)] @@ -93,7 +93,7 @@ mod tests { fn configure( &self, _cfg: &mut ConfigBag, - _inters: &mut Interceptors, + _inters: &mut InterceptorRegistrar, ) -> Result<(), BoxError> { todo!() } diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs index 270c1411b5..4b291a87d4 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs @@ -73,10 +73,10 @@ pub async fn invoke( let context = InterceptorContext::<()>::new(input); // Client configuration - handle_err!(context => runtime_plugins.apply_client_configuration(cfg, &mut interceptors)); + handle_err!(context => runtime_plugins.apply_client_configuration(cfg, interceptors.client_interceptors_mut())); handle_err!(context => interceptors.client_read_before_execution(&context, cfg)); // Operation configuration - handle_err!(context => runtime_plugins.apply_operation_configuration(cfg, &mut interceptors)); + handle_err!(context => runtime_plugins.apply_operation_configuration(cfg, interceptors.operation_interceptors_mut())); handle_err!(context => interceptors.operation_read_before_execution(&context, cfg)); let operation_timeout_config = cfg.maybe_timeout_config(TimeoutKind::Operation); @@ -216,7 +216,7 @@ async fn make_an_attempt( Ok(checkpoint) } -#[cfg(all(test, feature = "test-util"))] +#[cfg(all(test, feature = "test-util", feature = "anonymous-auth"))] mod tests { use super::invoke; use crate::client::orchestrator::endpoints::{ @@ -234,14 +234,13 @@ mod tests { }; use aws_smithy_runtime_api::client::interceptors::context::{Error, Output}; use aws_smithy_runtime_api::client::interceptors::{ - Interceptor, InterceptorContext, Interceptors, + Interceptor, InterceptorContext, InterceptorRegistrar, SharedInterceptor, }; use aws_smithy_runtime_api::client::orchestrator::ConfigBagAccessors; use aws_smithy_runtime_api::client::runtime_plugin::{BoxError, RuntimePlugin, RuntimePlugins}; use aws_smithy_runtime_api::config_bag::ConfigBag; use aws_smithy_runtime_api::type_erasure::TypeErasedBox; use http::StatusCode; - use std::sync::Arc; use tracing_test::traced_test; fn new_request_serializer() -> CannedRequestSerializer { @@ -269,7 +268,7 @@ mod tests { fn configure( &self, cfg: &mut ConfigBag, - _interceptors: &mut Interceptors, + _interceptors: &mut InterceptorRegistrar, ) -> Result<(), BoxError> { cfg.set_request_serializer(new_request_serializer()); cfg.set_response_deserializer(new_response_deserializer()); @@ -318,11 +317,11 @@ mod tests { fn configure( &self, _cfg: &mut ConfigBag, - interceptors: &mut Interceptors, + interceptors: &mut InterceptorRegistrar, ) -> Result<(), BoxError> { - interceptors.register_client_interceptor(Arc::new(FailingInterceptorA)); - interceptors.register_operation_interceptor(Arc::new(FailingInterceptorB)); - interceptors.register_operation_interceptor(Arc::new(FailingInterceptorC)); + interceptors.register(SharedInterceptor::new(FailingInterceptorA)); + interceptors.register(SharedInterceptor::new(FailingInterceptorB)); + interceptors.register(SharedInterceptor::new(FailingInterceptorC)); Ok(()) } diff --git a/rust-runtime/aws-smithy-runtime/src/client/runtime_plugin/anonymous_auth.rs b/rust-runtime/aws-smithy-runtime/src/client/runtime_plugin/anonymous_auth.rs index 7e958bbbb1..1fbc44b1fd 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/runtime_plugin/anonymous_auth.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/runtime_plugin/anonymous_auth.rs @@ -13,7 +13,7 @@ use aws_smithy_runtime_api::client::auth::{ AuthSchemeId, HttpAuthScheme, HttpAuthSchemes, HttpRequestSigner, }; use aws_smithy_runtime_api::client::identity::{Identity, IdentityResolver, IdentityResolvers}; -use aws_smithy_runtime_api::client::interceptors::Interceptors; +use aws_smithy_runtime_api::client::interceptors::InterceptorRegistrar; use aws_smithy_runtime_api::client::orchestrator::{BoxError, ConfigBagAccessors, HttpRequest}; use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin; use aws_smithy_runtime_api::config_bag::ConfigBag; @@ -43,7 +43,7 @@ impl RuntimePlugin for AnonymousAuthRuntimePlugin { fn configure( &self, cfg: &mut ConfigBag, - _interceptors: &mut Interceptors, + _interceptors: &mut InterceptorRegistrar, ) -> Result<(), BoxError> { cfg.set_auth_option_resolver_params(StaticAuthOptionResolverParams::new().into()); cfg.set_auth_option_resolver(StaticAuthOptionResolver::new(vec![ diff --git a/rust-runtime/aws-smithy-runtime/src/client/test_util/deserializer.rs b/rust-runtime/aws-smithy-runtime/src/client/test_util/deserializer.rs index 0c20268150..3c7bc1610c 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/test_util/deserializer.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/test_util/deserializer.rs @@ -4,7 +4,7 @@ */ use aws_smithy_runtime_api::client::interceptors::context::{Error, Output}; -use aws_smithy_runtime_api::client::interceptors::Interceptors; +use aws_smithy_runtime_api::client::interceptors::InterceptorRegistrar; use aws_smithy_runtime_api::client::orchestrator::{ ConfigBagAccessors, HttpResponse, ResponseDeserializer, }; @@ -44,7 +44,7 @@ impl RuntimePlugin for CannedResponseDeserializer { fn configure( &self, cfg: &mut ConfigBag, - _interceptors: &mut Interceptors, + _interceptors: &mut InterceptorRegistrar, ) -> Result<(), BoxError> { cfg.set_response_deserializer(Self { inner: Mutex::new(self.take()), diff --git a/rust-runtime/aws-smithy-runtime/src/client/test_util/serializer.rs b/rust-runtime/aws-smithy-runtime/src/client/test_util/serializer.rs index 50e20b9022..d9a2659782 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/test_util/serializer.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/test_util/serializer.rs @@ -4,7 +4,7 @@ */ use aws_smithy_runtime_api::client::interceptors::context::Input; -use aws_smithy_runtime_api::client::interceptors::Interceptors; +use aws_smithy_runtime_api::client::interceptors::InterceptorRegistrar; use aws_smithy_runtime_api::client::orchestrator::{ ConfigBagAccessors, HttpRequest, RequestSerializer, }; @@ -51,7 +51,7 @@ impl RuntimePlugin for CannedRequestSerializer { fn configure( &self, cfg: &mut ConfigBag, - _interceptors: &mut Interceptors, + _interceptors: &mut InterceptorRegistrar, ) -> Result<(), BoxError> { cfg.set_request_serializer(Self { inner: Mutex::new(self.take()),