From d75382789a3d58e6c832d59dd0cc7cce90477a58 Mon Sep 17 00:00:00 2001 From: ysaito1001 Date: Fri, 30 Jun 2023 19:40:27 -0500 Subject: [PATCH] Implement the remaining part of supporting operation level configuration (#2814) ## Motivation and Context Implements the actual meat of `config_override` introduced [as a skeleton in the past](https://github.com/awslabs/smithy-rs/pull/2589). ## Description This PR enables operation-level config via `config_override` on a customizable operation (see [config-override.rs](https://github.com/awslabs/smithy-rs/blob/8105dd46b43854e7909aed82c85223414bc85df5/aws/sdk/integration-tests/s3/tests/config-override.rs) integration tests for example code snippets). The way it's implemented is through `ConfigOverrideRuntimePlugin`. The plugin holds onto two things: a `Builder` passed to `config_override` and a `FrozenLayer` derived from a service config (the latter is primarily for retrieving default values understood by a service config). The plugin then implements the `RuntimePlugin` trait to generate its own `FrozenLayer` that contains operation-level orchestrator components. That `FrozenLayer` will then be added to a config bag later in the orchestrator execution in a way that it takes precedence over the client-level configuration (see [register_default_runtime_plugins](https://github.com/awslabs/smithy-rs/blob/8105dd46b43854e7909aed82c85223414bc85df5/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationGenerator.kt#L65-L71)). A couple of things to note: - The size of `ConfigOverrideRuntimePlugin::config` code-generated is getting large. Maybe each customization defines a helper function instead of inlining logic directly in `config` and we let the `config` method call those generated helpers. - The PR does not handle a case where `retry_partition` within `config_override` since it's currently `#[doc(hidden)]`, e.g. ``` client .some_operation() .customize() .await .unwrap() .config_override(Config::builder().retry_partition(/* ... */)) ... ``` ## Testing - Added tests in Kotlin [ConfigOverrideRuntimePluginGeneratorTest.kt](https://github.com/awslabs/smithy-rs/pull/2814/files#diff-04a76a43c2adb3a2ee37443157c68ec6dad77fab2484722b370a7ba14cf02086) and [CredentialCacheConfigTest.kt](https://github.com/awslabs/smithy-rs/pull/2814/files#diff-32246072688cd11391fa10cd9cb38a80ed88b587e95037225dbe9f1a482ebc5d). ~~These tests are minimal in that they only check if the appropriate orchestrator components are inserted into a config override layer when a user calls a certain builder method as part of `config_override`.~~ - Added integration tests [config-override.rs](https://github.com/awslabs/smithy-rs/pull/2814/files#diff-6fd7a1e70b1c3fa3e9c747925f3fc7a6ce0f7b801bd6bc46c54eec44150f5158) ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._ --------- Co-authored-by: Yuki Saito --- .../smithy/rustsdk/AwsPresigningDecorator.kt | 3 +- .../amazon/smithy/rustsdk/CredentialCaches.kt | 30 +++ .../rustsdk/CredentialCacheConfigTest.kt | 188 +++++++++++++ .../amazon/smithy/rustsdk/TestUtil.kt | 7 + .../s3/tests/config-override.rs | 121 +++++++++ .../HttpConnectorConfigDecorator.kt | 39 +++ .../InterceptorConfigCustomization.kt | 2 +- .../ResiliencyConfigCustomization.kt | 26 +- .../endpoint/EndpointConfigCustomization.kt | 18 ++ .../ConfigOverrideRuntimePluginGenerator.kt | 82 ++++++ .../smithy/generators/OperationGenerator.kt | 6 +- .../smithy/generators/PaginatorGenerator.kt | 3 +- .../smithy/generators/ServiceGenerator.kt | 3 +- .../client/FluentClientGenerator.kt | 3 +- .../config/ServiceConfigGenerator.kt | 56 ++-- ...onfigOverrideRuntimePluginGeneratorTest.kt | 247 ++++++++++++++++++ .../generators/PaginatorGeneratorTest.kt | 13 +- .../codegen/core/rustlang/CargoDependency.kt | 2 + .../smithy/rust/codegen/core/testutil/Rust.kt | 2 + .../aws-smithy-types/src/config_bag.rs | 7 + 20 files changed, 800 insertions(+), 58 deletions(-) create mode 100644 aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/CredentialCacheConfigTest.kt create mode 100644 aws/sdk/integration-tests/s3/tests/config-override.rs create mode 100644 codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ConfigOverrideRuntimePluginGenerator.kt create mode 100644 codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ConfigOverrideRuntimePluginGeneratorTest.kt diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsPresigningDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsPresigningDecorator.kt index 5ffd15a6a8..ef59287b35 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsPresigningDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsPresigningDecorator.kt @@ -322,7 +322,8 @@ class AwsPresignedFluentBuilderMethod( let runtime_plugins = #{Operation}::operation_runtime_plugins( self.handle.runtime_plugins.clone(), - self.config_override + &self.handle.conf, + self.config_override, ) .with_client_plugin(#{SigV4PresigningRuntimePlugin}::new(presigning_config, #{payload_override})) #{alternate_presigning_serializer_registration}; diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialCaches.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialCaches.kt index 053e7ec7e6..b31c5329ea 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialCaches.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialCaches.kt @@ -191,6 +191,36 @@ class CredentialCacheConfig(codegenContext: ClientCodegenContext) : ConfigCustom } } + is ServiceConfig.OperationConfigOverride -> { + rustTemplate( + """ + match ( + layer + .load::<#{CredentialsCache}>() + .cloned(), + layer + .load::<#{SharedCredentialsProvider}>() + .cloned(), + ) { + (#{None}, #{None}) => {} + (#{None}, _) => { + panic!("also specify `.credentials_cache` when overriding credentials provider for the operation"); + } + (_, #{None}) => { + panic!("also specify `.credentials_provider` when overriding credentials cache for the operation"); + } + ( + #{Some}(credentials_cache), + #{Some}(credentials_provider), + ) => { + layer.store_put(credentials_cache.create_cache(credentials_provider)); + } + } + """, + *codegenScope, + ) + } + else -> emptySection } } diff --git a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/CredentialCacheConfigTest.kt b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/CredentialCacheConfigTest.kt new file mode 100644 index 0000000000..c598c1ab0d --- /dev/null +++ b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/CredentialCacheConfigTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rustsdk + +import org.junit.jupiter.api.Test +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.core.testutil.testModule +import software.amazon.smithy.rust.codegen.core.testutil.tokioTest +import software.amazon.smithy.rust.codegen.core.testutil.unitTest + +internal class CredentialCacheConfigTest { + private val model = """ + namespace com.example + use aws.protocols#awsJson1_0 + use aws.api#service + use smithy.rules#endpointRuleSet + + @service(sdkId: "Some Value") + @awsJson1_0 + @endpointRuleSet({ + "version": "1.0", + "rules": [{ + "type": "endpoint", + "conditions": [{"fn": "isSet", "argv": [{"ref": "Region"}]}], + "endpoint": { "url": "https://example.com" } + }], + "parameters": { + "Region": { "required": false, "type": "String", "builtIn": "AWS::Region" }, + } + }) + service HelloService { + operations: [SayHello], + version: "1" + } + + @optionalAuth + operation SayHello { input: TestInput } + structure TestInput { + foo: String, + } + """.asSmithyModel() + + @Test + fun `config override for credentials`() { + awsSdkIntegrationTest(model, defaultToOrchestrator = true) { clientCodegenContext, rustCrate -> + val runtimeConfig = clientCodegenContext.runtimeConfig + val codegenScope = arrayOf( + *RuntimeType.preludeScope, + "Credentials" to AwsRuntimeType.awsCredentialTypesTestUtil(runtimeConfig) + .resolve("Credentials"), + "CredentialsCache" to AwsRuntimeType.awsCredentialTypes(runtimeConfig) + .resolve("cache::CredentialsCache"), + "ProvideCachedCredentials" to AwsRuntimeType.awsCredentialTypes(runtimeConfig) + .resolve("cache::ProvideCachedCredentials"), + "RuntimePlugin" to RuntimeType.smithyRuntimeApi(runtimeConfig) + .resolve("client::runtime_plugin::RuntimePlugin"), + "SharedCredentialsCache" to AwsRuntimeType.awsCredentialTypes(runtimeConfig) + .resolve("cache::SharedCredentialsCache"), + "SharedCredentialsProvider" to AwsRuntimeType.awsCredentialTypes(runtimeConfig) + .resolve("provider::SharedCredentialsProvider"), + ) + rustCrate.testModule { + unitTest( + "test_overriding_only_credentials_provider_should_panic", + additionalAttributes = listOf(Attribute.shouldPanic("also specify `.credentials_cache` when overriding credentials provider for the operation")), + ) { + rustTemplate( + """ + use #{RuntimePlugin}; + + let client_config = crate::config::Config::builder().build(); + let config_override = + crate::config::Config::builder().credentials_provider(#{Credentials}::for_tests()); + let sut = crate::config::ConfigOverrideRuntimePlugin { + client_config: client_config.config().unwrap(), + config_override, + }; + + // this should cause `panic!` + let _ = sut.config().unwrap(); + """, + *codegenScope, + ) + } + + unitTest( + "test_overriding_only_credentials_cache_should_panic", + additionalAttributes = listOf(Attribute.shouldPanic("also specify `.credentials_provider` when overriding credentials cache for the operation")), + ) { + rustTemplate( + """ + use #{RuntimePlugin}; + + let client_config = crate::config::Config::builder().build(); + let config_override = crate::config::Config::builder() + .credentials_cache(#{CredentialsCache}::no_caching()); + let sut = crate::config::ConfigOverrideRuntimePlugin { + client_config: client_config.config().unwrap(), + config_override, + }; + + // this should cause `panic!` + let _ = sut.config().unwrap(); + """, + *codegenScope, + ) + } + + tokioTest("test_overriding_cache_and_provider_leads_to_shared_credentials_cache_in_layer") { + rustTemplate( + """ + use #{ProvideCachedCredentials}; + use #{RuntimePlugin}; + + let client_config = crate::config::Config::builder() + .credentials_provider(#{Credentials}::for_tests()) + .build(); + let client_config_layer = client_config.config().unwrap(); + + // make sure test credentials are set in the client config level + assert_eq!(#{Credentials}::for_tests(), + client_config_layer + .load::<#{SharedCredentialsCache}>() + .unwrap() + .provide_cached_credentials() + .await + .unwrap() + ); + + let credentials = #{Credentials}::new( + "test", + "test", + #{None}, + #{None}, + "test", + ); + let config_override = crate::config::Config::builder() + .credentials_cache(#{CredentialsCache}::lazy()) + .credentials_provider(credentials.clone()); + let sut = crate::config::ConfigOverrideRuntimePlugin { + client_config: client_config_layer, + config_override, + }; + let sut_layer = sut.config().unwrap(); + + // make sure `.provide_cached_credentials` returns credentials set through `config_override` + assert_eq!(credentials, + sut_layer + .load::<#{SharedCredentialsCache}>() + .unwrap() + .provide_cached_credentials() + .await + .unwrap() + ); + """, + *codegenScope, + ) + } + + unitTest("test_not_overriding_cache_and_provider_leads_to_no_shared_credentials_cache_in_layer") { + rustTemplate( + """ + use #{RuntimePlugin}; + + let client_config = crate::config::Config::builder().build(); + let config_override = crate::config::Config::builder(); + let sut = crate::config::ConfigOverrideRuntimePlugin { + client_config: client_config.config().unwrap(), + config_override, + }; + let sut_layer = sut.config().unwrap(); + assert!(sut_layer + .load::<#{SharedCredentialsCache}>() + .is_none()); + """, + *codegenScope, + ) + } + } + } + } +} diff --git a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestUtil.kt b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestUtil.kt index fc40eec7cd..0dabd0033e 100644 --- a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestUtil.kt +++ b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestUtil.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.rustsdk import software.amazon.smithy.model.Model import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.model.node.StringNode import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.ClientRustSettings import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest @@ -17,6 +18,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams import software.amazon.smithy.rust.codegen.core.testutil.TestRuntimeConfig import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.core.util.letIf import java.io.File // In aws-sdk-codegen, the working dir when gradle runs tests is actually `./aws`. So, to find the smithy runtime, we need @@ -35,8 +37,10 @@ fun awsTestCodegenContext(model: Model? = null, settings: ClientRustSettings? = settings = settings ?: testClientRustSettings(runtimeConfig = AwsTestRuntimeConfig), ) +// TODO(enableNewSmithyRuntimeCleanup): Remove defaultToOrchestrator once the runtime switches to the orchestrator fun awsSdkIntegrationTest( model: Model, + defaultToOrchestrator: Boolean = false, test: (ClientCodegenContext, RustCrate) -> Unit = { _, _ -> }, ) = clientIntegrationTest( @@ -58,6 +62,9 @@ fun awsSdkIntegrationTest( "codegen", ObjectNode.builder() .withMember("includeFluentClient", false) + .letIf(defaultToOrchestrator) { + it.withMember("enableNewSmithyRuntime", StringNode.from("orchestrator")) + } .build(), ).build(), ), diff --git a/aws/sdk/integration-tests/s3/tests/config-override.rs b/aws/sdk/integration-tests/s3/tests/config-override.rs new file mode 100644 index 0000000000..b3d93be867 --- /dev/null +++ b/aws/sdk/integration-tests/s3/tests/config-override.rs @@ -0,0 +1,121 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_credential_types::provider::SharedCredentialsProvider; +use aws_sdk_s3::config::{Credentials, Region}; +use aws_sdk_s3::Client; +use aws_smithy_client::test_connection::{capture_request, CaptureRequestReceiver}; +use aws_types::SdkConfig; + +// TODO(enableNewSmithyRuntimeCleanup): Remove this attribute once #[cfg(aws_sdk_orchestrator_mode)] +// has been removed +#[allow(dead_code)] +fn test_client() -> (CaptureRequestReceiver, Client) { + let (conn, captured_request) = capture_request(None); + let sdk_config = SdkConfig::builder() + .credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests())) + .region(Region::new("us-west-2")) + .http_connector(conn) + .build(); + let client = Client::new(&sdk_config); + (captured_request, client) +} + +#[cfg(aws_sdk_orchestrator_mode)] +#[tokio::test] +async fn operation_overrides_force_path_style() { + let (captured_request, client) = test_client(); + let _ = client + .list_objects_v2() + .bucket("test-bucket") + .customize() + .await + .unwrap() + .config_override(aws_sdk_s3::config::Config::builder().force_path_style(true)) + .send() + .await; + assert_eq!( + captured_request.expect_request().uri().to_string(), + "https://s3.us-west-2.amazonaws.com/test-bucket/?list-type=2" + ); +} + +#[cfg(aws_sdk_orchestrator_mode)] +#[tokio::test] +async fn operation_overrides_fips() { + let (captured_request, client) = test_client(); + let _ = client + .list_objects_v2() + .bucket("test-bucket") + .customize() + .await + .unwrap() + .config_override(aws_sdk_s3::config::Config::builder().use_fips(true)) + .send() + .await; + assert_eq!( + captured_request.expect_request().uri().to_string(), + "https://test-bucket.s3-fips.us-west-2.amazonaws.com/?list-type=2" + ); +} + +#[cfg(aws_sdk_orchestrator_mode)] +#[tokio::test] +async fn operation_overrides_dual_stack() { + let (captured_request, client) = test_client(); + let _ = client + .list_objects_v2() + .bucket("test-bucket") + .customize() + .await + .unwrap() + .config_override(aws_sdk_s3::config::Config::builder().use_dual_stack(true)) + .send() + .await; + assert_eq!( + captured_request.expect_request().uri().to_string(), + "https://test-bucket.s3.dualstack.us-west-2.amazonaws.com/?list-type=2" + ); +} + +// TODO(enableNewSmithyRuntimeCleanup): Comment in the following test once Handle is no longer +// accessed in ServiceRuntimePlugin::config. Currently, a credentials cache created for a single +// operation invocation is not picked up by an identity resolver. +/* +#[cfg(aws_sdk_orchestrator_mode)] +#[tokio::test] +async fn operation_overrides_credentials_provider() { + let (captured_request, client) = test_client(); + let _ = client + .list_objects_v2() + .bucket("test-bucket") + .customize() + .await + .unwrap() + .config_override(aws_sdk_s3::config::Config::builder().credentials_provider(Credentials::new( + "test", + "test", + Some("test".into()), + Some(std::time::UNIX_EPOCH + std::time::Duration::from_secs(1669257290 + 3600)), + "test", + ))) + .request_time_for_tests(std::time::UNIX_EPOCH + std::time::Duration::from_secs(1669257290)) + .send() + .await; + + let request = captured_request.expect_request(); + let actual_auth = + std::str::from_utf8(request.headers().get("authorization").unwrap().as_bytes()).unwrap(); + // signature would be f98cc3911dfba0daabf4343152f456bff9ecd3888a3068a1346d26949cb8f9e5 + // if we used `Credentials::for_tests()` + let expected_sig = "Signature=d7e7be63efc37c5bab5eda121999cd1c9a95efdde0cc1ce7c1b8761051cc3cbd"; + assert!( + actual_auth.contains(expected_sig), + "authorization header signature did not match expected signature: expected {} but not found in {}", + expected_sig, + actual_auth, + ); +} +*/ 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 2c80bfd0cb..1153c27770 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 @@ -217,6 +217,45 @@ private class HttpConnectorConfigCustomization( } } + is ServiceConfig.OperationConfigOverride -> writable { + if (runtimeMode.defaultToOrchestrator) { + rustTemplate( + """ + if let #{Some}(http_connector) = + layer.load::<#{HttpConnector}>() + { + let sleep_impl = layer + .load::<#{SharedAsyncSleep}>() + .or_else(|| { + self.client_config + .load::<#{SharedAsyncSleep}>() + }) + .cloned(); + let timeout_config = layer + .load::<#{TimeoutConfig}>() + .or_else(|| { + self.client_config + .load::<#{TimeoutConfig}>() + }) + .expect("timeout config should be set either in `config_override` or in the client config"); + let connector_settings = + #{ConnectorSettings}::from_timeout_config( + timeout_config, + ); + if let #{Some}(conn) = http_connector.connector(&connector_settings, sleep_impl) { + let connection: #{DynConnector} = #{DynConnector}::new(#{DynConnectorAdapter}::new( + // TODO(enableNewSmithyRuntimeCleanup): Replace the tower-based DynConnector and remove DynConnectorAdapter when deleting the middleware implementation + conn + )); + layer.set_connector(connection); + } + } + """, + *codegenScope, + ) + } + } + else -> emptySection } } 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 index b193172b55..692b874517 100644 --- 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 @@ -172,7 +172,7 @@ class InterceptorConfigCustomization(codegenContext: ClientCodegenContext) : Con is ServiceConfig.RuntimePluginInterceptors -> rust( """ - ${section.interceptors}.extend(self.interceptors.iter().cloned()); + ${section.interceptors}.extend(${section.interceptorsField}.interceptors.iter().cloned()); """, ) diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ResiliencyConfigCustomization.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ResiliencyConfigCustomization.kt index 9cb2f86950..dc20049c46 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ResiliencyConfigCustomization.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ResiliencyConfigCustomization.kt @@ -377,7 +377,7 @@ class ResiliencyConfigCustomization(private val codegenContext: ClientCodegenCon } if retry_config.mode() == #{RetryMode}::Adaptive { - if let Some(time_source) = layer.load::<#{SharedTimeSource}>().cloned() { + if let #{Some}(time_source) = layer.load::<#{SharedTimeSource}>().cloned() { let seconds_since_unix_epoch = time_source .now() .duration_since(#{SystemTime}::UNIX_EPOCH) @@ -396,6 +396,12 @@ class ResiliencyConfigCustomization(private val codegenContext: ClientCodegenCon let token_bucket = TOKEN_BUCKET.get_or_init(token_bucket_partition, #{TokenBucket}::default); layer.store_put(token_bucket); layer.set_retry_strategy(#{DynRetryStrategy}::new(#{StandardRetryStrategy}::new(&retry_config))); + + // TODO(enableNewSmithyRuntimeCleanup): Should not need to provide a default once smithy-rs##2770 + // is resolved + if layer.load::<#{TimeoutConfig}>().is_none() { + layer.store_put(#{TimeoutConfig}::disabled()); + } """, *codegenScope, ) @@ -414,6 +420,24 @@ class ResiliencyConfigCustomization(private val codegenContext: ClientCodegenCon } } + is ServiceConfig.OperationConfigOverride -> { + if (runtimeMode.defaultToOrchestrator) { + rustTemplate( + """ + if let #{Some}(retry_config) = layer + .load::<#{RetryConfig}>() + .cloned() + { + layer.set_retry_strategy( + #{DynRetryStrategy}::new(#{StandardRetryStrategy}::new(&retry_config)) + ); + } + """, + *codegenScope, + ) + } + } + else -> emptySection } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointConfigCustomization.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointConfigCustomization.kt index a6b07ecaaa..a55c5b3e7d 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointConfigCustomization.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointConfigCustomization.kt @@ -240,6 +240,24 @@ internal class EndpointConfigCustomization( } } + is ServiceConfig.OperationConfigOverride -> { + if (runtimeMode.defaultToOrchestrator) { + rustTemplate( + """ + if let #{Some}(resolver) = layer + .load::<$sharedEndpointResolver>() + .cloned() + { + let endpoint_resolver = #{DynEndpointResolver}::new( + #{DefaultEndpointResolver}::<#{Params}>::new(resolver)); + layer.set_endpoint_resolver(endpoint_resolver); + } + """, + *codegenScope, + ) + } + } + else -> emptySection } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ConfigOverrideRuntimePluginGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ConfigOverrideRuntimePluginGenerator.kt new file mode 100644 index 0000000000..edfe94af95 --- /dev/null +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ConfigOverrideRuntimePluginGenerator.kt @@ -0,0 +1,82 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.client.smithy.generators + +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +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.RustWriter +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.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.customize.writeCustomizations + +class ConfigOverrideRuntimePluginGenerator( + codegenContext: ClientCodegenContext, +) { + private val moduleUseName = codegenContext.moduleUseName() + private val codegenScope = codegenContext.runtimeConfig.let { rc -> + val runtimeApi = RuntimeType.smithyRuntimeApi(rc) + val smithyTypes = RuntimeType.smithyTypes(rc) + arrayOf( + *RuntimeType.preludeScope, + "CloneableLayer" to smithyTypes.resolve("config_bag::CloneableLayer"), + "ConfigBagAccessors" to runtimeApi.resolve("client::config_bag_accessors::ConfigBagAccessors"), + "FrozenLayer" to smithyTypes.resolve("config_bag::FrozenLayer"), + "InterceptorRegistrar" to runtimeApi.resolve("client::interceptors::InterceptorRegistrar"), + "Layer" to smithyTypes.resolve("config_bag::Layer"), + "RuntimePlugin" to runtimeApi.resolve("client::runtime_plugin::RuntimePlugin"), + ) + } + + fun render(writer: RustWriter, customizations: List) { + writer.rustTemplate( + """ + /// A plugin that enables configuration for a single operation invocation + /// + /// The `config` method will return a `FrozenLayer` by storing values from `config_override`. + /// In the case of default values requested, they will be obtained from `client_config`. + ##[derive(Debug)] + pub(crate) struct ConfigOverrideRuntimePlugin { + pub(crate) config_override: Builder, + pub(crate) client_config: #{FrozenLayer}, + } + + impl #{RuntimePlugin} for ConfigOverrideRuntimePlugin { + fn config(&self) -> #{Option}<#{FrozenLayer}> { + use #{ConfigBagAccessors}; + + ##[allow(unused_mut)] + let layer: #{Layer} = self + .config_override + .inner + .clone() + .into(); + let mut layer = layer.with_name("$moduleUseName::config::ConfigOverrideRuntimePlugin"); + #{config} + + #{Some}(layer.freeze()) + } + + fn interceptors(&self, _interceptors: &mut #{InterceptorRegistrar}) { + #{interceptors} + } + } + + """, + *codegenScope, + "config" to writable { + writeCustomizations( + customizations, + ServiceConfig.OperationConfigOverride("layer"), + ) + }, + "interceptors" to writable { + writeCustomizations(customizations, ServiceConfig.RuntimePluginInterceptors("_interceptors", "self.config_override")) + }, + ) + } +} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationGenerator.kt index 5bf9c8615b..a0087b3d2a 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationGenerator.kt @@ -155,11 +155,15 @@ open class OperationGenerator( pub(crate) fn operation_runtime_plugins( client_runtime_plugins: #{RuntimePlugins}, + client_config: &crate::config::Config, config_override: #{Option}, ) -> #{RuntimePlugins} { let mut runtime_plugins = client_runtime_plugins.with_operation_plugin(Self::new()); if let Some(config_override) = config_override { - runtime_plugins = runtime_plugins.with_operation_plugin(config_override); + runtime_plugins = runtime_plugins.with_operation_plugin(crate::config::ConfigOverrideRuntimePlugin { + config_override, + client_config: #{RuntimePlugin}::config(client_config).expect("frozen layer should exist in client config"), + }) } runtime_plugins #{additional_runtime_plugins} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt index 5e18a99114..3f86fe720d 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt @@ -242,7 +242,8 @@ class PaginatorGenerator private constructor( """ let runtime_plugins = #{operation}::operation_runtime_plugins( handle.runtime_plugins.clone(), - None, + &handle.conf, + #{None}, ); """, *codegenScope, diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt index 571abbd508..1c85d9047b 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt @@ -55,7 +55,8 @@ class ServiceGenerator( .render(this, decorator.serviceRuntimePluginCustomizations(codegenContext, emptyList())) serviceConfigGenerator.renderRuntimePluginImplForSelf(this) - serviceConfigGenerator.renderRuntimePluginImplForBuilder(this) + ConfigOverrideRuntimePluginGenerator(codegenContext) + .render(this, decorator.configCustomizations(codegenContext, listOf())) } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt index 3292ae10bb..6fe9978f78 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt @@ -518,7 +518,8 @@ class FluentClientGenerator( let input = self.inner.build().map_err(#{SdkError}::construction_failure)?; let runtime_plugins = #{Operation}::operation_runtime_plugins( self.handle.runtime_plugins.clone(), - self.config_override + &self.handle.conf, + self.config_override, ); #{Operation}::orchestrate(&runtime_plugins, input).await } 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 69634adba4..148353269f 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 @@ -13,7 +13,6 @@ import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.traits.IdempotencyTokenTrait import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule -import software.amazon.smithy.rust.codegen.client.smithy.customizations.codegenScope import software.amazon.smithy.rust.codegen.client.smithy.customize.TestUtilFeature import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter @@ -92,20 +91,20 @@ sealed class ServiceConfig(name: String) : Section(name) { */ object BuilderBuild : ServiceConfig("BuilderBuild") - // TODO(enableNewSmithyRuntimeLaunch): This is temporary until config builder is backed by a CloneableLayer. - // It is needed because certain config fields appear explicitly regardless of the smithy runtime mode, e.g. - // interceptors. The [BuilderBuild] section is bifurcated depending on the runtime mode (in the orchestrator mode, - // storing a field into a frozen layer and in the middleware moving it into a corresponding service config field) - // so we need a different temporary section to always move a field from a builder to service config within the - // build method. + /** + * A section for customizing individual fields in the initializer of Config + */ object BuilderBuildExtras : ServiceConfig("BuilderBuildExtras") /** - * A section for setting up a field to be used by RuntimePlugin + * A section for setting up a field to be used by ConfigOverrideRuntimePlugin */ - data class RuntimePluginConfig(val cfg: String) : ServiceConfig("ToRuntimePlugin") + data class OperationConfigOverride(val cfg: String) : ServiceConfig("ToRuntimePlugin") - data class RuntimePluginInterceptors(val interceptors: String) : ServiceConfig("ToRuntimePluginInterceptors") + /** + * A section for appending additional runtime plugins, stored in [interceptorsField], to [interceptors] + */ + data class RuntimePluginInterceptors(val interceptors: String, val interceptorsField: String) : ServiceConfig("ToRuntimePluginInterceptors") /** * A section for extra functionality that needs to be defined with the config module @@ -262,7 +261,7 @@ fun standardConfigParam(param: ConfigParam, codegenContext: ClientCodegenContext } } - is ServiceConfig.RuntimePluginConfig -> emptySection + is ServiceConfig.OperationConfigOverride -> emptySection else -> emptySection } @@ -434,7 +433,10 @@ class ServiceConfigGenerator( // requiring that items created and stored _during_ the build method be `Clone`, since they // will soon be part of a `FrozenLayer` owned by the service config. So we will convert the // current `CloneableLayer` into a `Layer` that does not impose the `Clone` requirement. - let mut layer: #{Layer} = self.inner.into(); + let layer: #{Layer} = self + .inner + .into(); + let mut layer = layer.with_name("$moduleUseName::config::config"); """, *codegenScope, ) @@ -478,35 +480,9 @@ class ServiceConfigGenerator( """, *codegenScope, - "config" to writable { writeCustomizations(customizations, ServiceConfig.RuntimePluginConfig("cfg")) }, - "interceptors" to writable { - writeCustomizations(customizations, ServiceConfig.RuntimePluginInterceptors("_interceptors")) - }, - ) - } - - fun renderRuntimePluginImplForBuilder(writer: RustWriter) { - writer.rustTemplate( - """ - impl #{RuntimePlugin} for Builder { - fn config(&self) -> #{Option}<#{FrozenLayer}> { - // TODO(enableNewSmithyRuntimeLaunch): Put into `cfg` the fields in `self.config_override` that are not `None` - ##[allow(unused_mut)] - let mut cfg = #{CloneableLayer}::new("service config"); - #{config} - #{Some}(cfg.freeze()) - } - - fn interceptors(&self, _interceptors: &mut #{InterceptorRegistrar}) { - #{interceptors} - } - } - - """, - *codegenScope, - "config" to writable { writeCustomizations(customizations, ServiceConfig.RuntimePluginConfig("cfg")) }, + "config" to writable { writeCustomizations(customizations, ServiceConfig.OperationConfigOverride("cfg")) }, "interceptors" to writable { - writeCustomizations(customizations, ServiceConfig.RuntimePluginInterceptors("_interceptors")) + writeCustomizations(customizations, ServiceConfig.RuntimePluginInterceptors("_interceptors", "self")) }, ) } diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ConfigOverrideRuntimePluginGeneratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ConfigOverrideRuntimePluginGeneratorTest.kt new file mode 100644 index 0000000000..b1ee6311a6 --- /dev/null +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ConfigOverrideRuntimePluginGeneratorTest.kt @@ -0,0 +1,247 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.client.smithy.generators + +import org.junit.jupiter.api.Test +import software.amazon.smithy.rust.codegen.client.testutil.TestCodegenSettings +import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.Companion.smithyRuntimeApiTestUtil +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope +import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.core.testutil.testModule +import software.amazon.smithy.rust.codegen.core.testutil.tokioTest +import software.amazon.smithy.rust.codegen.core.testutil.unitTest + +internal class ConfigOverrideRuntimePluginGeneratorTest { + private val model = """ + namespace com.example + use aws.protocols#awsJson1_0 + + @awsJson1_0 + service HelloService { + operations: [SayHello], + version: "1" + } + + @optionalAuth + operation SayHello { input: TestInput } + structure TestInput { + foo: String, + } + """.asSmithyModel() + + @Test + fun `operation overrides endpoint resolver`() { + clientIntegrationTest( + model, + params = IntegrationTestParams(additionalSettings = TestCodegenSettings.orchestratorMode()), + ) { clientCodegenContext, rustCrate -> + val runtimeConfig = clientCodegenContext.runtimeConfig + val codegenScope = arrayOf( + *preludeScope, + "ConfigBagAccessors" to RuntimeType.configBagAccessors(runtimeConfig), + "EndpointResolverParams" to RuntimeType.smithyRuntimeApi(runtimeConfig) + .resolve("client::orchestrator::EndpointResolverParams"), + "RuntimePlugin" to RuntimeType.runtimePlugin(runtimeConfig), + ) + rustCrate.testModule { + addDependency(CargoDependency.Tokio.withFeature("test-util").toDevDependency()) + tokioTest("test_operation_overrides_endpoint_resolver") { + rustTemplate( + """ + use #{ConfigBagAccessors}; + use #{RuntimePlugin}; + + let expected_url = "http://localhost:1234/"; + let client_config = crate::config::Config::builder().build(); + let config_override = + crate::config::Config::builder().endpoint_resolver(expected_url); + let sut = crate::config::ConfigOverrideRuntimePlugin { + client_config: client_config.config().unwrap(), + config_override, + }; + let sut_layer = sut.config().unwrap(); + let endpoint_resolver = sut_layer.endpoint_resolver(); + let endpoint = endpoint_resolver + .resolve_endpoint(&#{EndpointResolverParams}::new(crate::config::endpoint::Params {})) + .await + .unwrap(); + + assert_eq!(expected_url, endpoint.url()); + """, + *codegenScope, + ) + } + } + } + } + + @Test + fun `operation overrides http connector`() { + clientIntegrationTest( + model, + params = IntegrationTestParams(additionalSettings = TestCodegenSettings.orchestratorMode()), + ) { clientCodegenContext, rustCrate -> + val runtimeConfig = clientCodegenContext.runtimeConfig + val codegenScope = arrayOf( + *preludeScope, + "ConfigBagAccessors" to RuntimeType.configBagAccessors(runtimeConfig), + "RuntimePlugin" to RuntimeType.runtimePlugin(runtimeConfig), + ) + rustCrate.testModule { + addDependency(CargoDependency.Tokio.withFeature("test-util").toDevDependency()) + tokioTest("test_operation_overrides_http_connection") { + rustTemplate( + """ + use #{AsyncSleep}; + + let (conn, captured_request) = #{capture_request}(#{None}); + let expected_url = "http://localhost:1234/"; + let client_config = crate::config::Config::builder() + .endpoint_resolver(expected_url) + .http_connector(#{NeverConnector}::new()) + .build(); + let client = crate::client::Client::from_conf(client_config.clone()); + let sleep = #{TokioSleep}::new(); + + let send = client.say_hello().send(); + let timeout = #{Timeout}::new( + send, + sleep.sleep(::std::time::Duration::from_millis(100)), + ); + + // sleep future should win because the other future `send` is backed by `NeverConnector`, + // yielding `Err` + matches!(timeout.await, Err(_)); + + let client = crate::client::Client::from_conf(client_config); + let customizable_send = client + .say_hello() + .customize() + .await + .unwrap() + .config_override(crate::config::Config::builder().http_connector(conn)) + .send(); + + let timeout = #{Timeout}::new( + customizable_send, + sleep.sleep(::std::time::Duration::from_millis(100)), + ); + + // `conn` should shadow `NeverConnector` by virtue of `config_override` + match timeout.await { + Ok(_) => { + assert_eq!( + expected_url, + captured_request.expect_request().uri().to_string() + ); + } + Err(_) => { + panic!("this arm should not be reached since the `customizable_send` future should win"); + } + } + """, + *codegenScope, + "AsyncSleep" to RuntimeType.smithyAsync(runtimeConfig).resolve("rt::sleep::AsyncSleep"), + "capture_request" to RuntimeType.captureRequest(runtimeConfig), + "NeverConnector" to RuntimeType.smithyClient(runtimeConfig) + .resolve("never::NeverConnector"), + "Timeout" to RuntimeType.smithyAsync(runtimeConfig).resolve("future::timeout::Timeout"), + "TokioSleep" to RuntimeType.smithyAsync(runtimeConfig) + .resolve("rt::sleep::TokioSleep"), + ) + } + } + } + } + + @Test + fun `operation overrides retry strategy`() { + clientIntegrationTest( + model, + params = IntegrationTestParams(additionalSettings = TestCodegenSettings.orchestratorMode()), + ) { clientCodegenContext, rustCrate -> + val runtimeConfig = clientCodegenContext.runtimeConfig + val codegenScope = arrayOf( + *preludeScope, + "AlwaysRetry" to RuntimeType.smithyRuntimeApi(runtimeConfig) + .resolve("client::retries::AlwaysRetry"), + "ConfigBag" to RuntimeType.smithyTypes(runtimeConfig).resolve("config_bag::ConfigBag"), + "ConfigBagAccessors" to RuntimeType.configBagAccessors(runtimeConfig), + "ErrorKind" to RuntimeType.smithyTypes(runtimeConfig).resolve("retry::ErrorKind"), + "InterceptorContext" to RuntimeType.interceptorContext(runtimeConfig), + "Layer" to RuntimeType.smithyTypes(runtimeConfig).resolve("config_bag::Layer"), + "OrchestratorError" to RuntimeType.smithyRuntimeApi(runtimeConfig) + .resolve("client::orchestrator::OrchestratorError"), + "RetryConfig" to RuntimeType.smithyTypes(clientCodegenContext.runtimeConfig) + .resolve("retry::RetryConfig"), + "RequestAttempts" to smithyRuntimeApiTestUtil(runtimeConfig).toType() + .resolve("client::request_attempts::RequestAttempts"), + "RetryClassifiers" to RuntimeType.smithyRuntimeApi(runtimeConfig) + .resolve("client::retries::RetryClassifiers"), + "RuntimePlugin" to RuntimeType.runtimePlugin(runtimeConfig), + "ShouldAttempt" to RuntimeType.smithyRuntimeApi(runtimeConfig) + .resolve("client::retries::ShouldAttempt"), + "TypeErasedBox" to RuntimeType.smithyTypes(runtimeConfig).resolve("type_erasure::TypeErasedBox"), + ) + rustCrate.testModule { + unitTest("test_operation_overrides_retry_strategy") { + rustTemplate( + """ + use #{ConfigBagAccessors}; + use #{RuntimePlugin}; + + let client_config = crate::config::Config::builder() + .retry_config(#{RetryConfig}::standard().with_max_attempts(3)) + .build(); + + let client_config_layer = client_config.config().unwrap(); + + let mut ctx = #{InterceptorContext}::new(#{TypeErasedBox}::new(())); + ctx.set_output_or_error(#{Err}(#{OrchestratorError}::other("doesn't matter"))); + let mut layer = #{Layer}::new("test"); + layer.store_put(#{RequestAttempts}::new(1)); + layer.set_retry_classifiers( + #{RetryClassifiers}::new().with_classifier(#{AlwaysRetry}(#{ErrorKind}::TransientError)), + ); + + let mut cfg = #{ConfigBag}::of_layers(vec![layer]); + cfg.push_shared_layer(client_config_layer.clone()); + + let retry = cfg.retry_strategy().unwrap(); + assert!(matches!( + retry.should_attempt_retry(&ctx, &cfg).unwrap(), + #{ShouldAttempt}::YesAfterDelay(_) + )); + + // sets `max_attempts` to 1 implicitly by using `disabled()`, forcing it to run out of + // attempts with respect to `RequestAttempts` set to 1 above + let config_override = crate::config::Config::builder() + .retry_config(#{RetryConfig}::disabled()); + let sut = crate::config::ConfigOverrideRuntimePlugin { + client_config: client_config_layer, + config_override, + }; + let sut_layer = sut.config().unwrap(); + cfg.push_shared_layer(sut_layer); + let retry = cfg.retry_strategy().unwrap(); + + assert!(matches!( + retry.should_attempt_retry(&ctx, &cfg).unwrap(), + #{ShouldAttempt}::No + )); + """, + *codegenScope, + ) + } + } + } + } +} diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGeneratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGeneratorTest.kt index 97f114fda0..0726c6870c 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGeneratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGeneratorTest.kt @@ -6,8 +6,7 @@ package software.amazon.smithy.rust.codegen.client.smithy.generators import org.junit.jupiter.api.Test -import software.amazon.smithy.model.node.ObjectNode -import software.amazon.smithy.model.node.StringNode +import software.amazon.smithy.rust.codegen.client.testutil.TestCodegenSettings import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.rust @@ -83,19 +82,11 @@ internal class PaginatorGeneratorTest { } } - private fun enableNewSmithyRuntime(): ObjectNode = ObjectNode.objectNodeBuilder() - .withMember( - "codegen", - ObjectNode.objectNodeBuilder() - .withMember("enableNewSmithyRuntime", StringNode.from("orchestrator")).build(), - ) - .build() - @Test fun `generate paginators that compile`() { clientIntegrationTest( model, - params = IntegrationTestParams(additionalSettings = enableNewSmithyRuntime()), + params = IntegrationTestParams(additionalSettings = TestCodegenSettings.orchestratorMode()), ) { clientCodegenContext, rustCrate -> rustCrate.integrationTest("paginators_generated") { Attribute.AllowUnusedImports.render(this) 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 d850b2cc26..b91020fe3f 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 @@ -289,6 +289,8 @@ data class CargoDependency( 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 smithyRuntimeApiTestUtil(runtimeConfig: RuntimeConfig) = + smithyRuntimeApi(runtimeConfig).toDevDependency().withFeature("test-util") 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/testutil/Rust.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt index fa98e87fcb..b40d5b8a06 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt @@ -215,10 +215,12 @@ fun RustWriter.unitTest( name: String, vararg args: Any, attribute: Attribute = Attribute.Test, + additionalAttributes: List = emptyList(), async: Boolean = false, block: Writable, ): RustWriter { attribute.render(this) + additionalAttributes.forEach { it.render(this) } if (async) { rust("async") } diff --git a/rust-runtime/aws-smithy-types/src/config_bag.rs b/rust-runtime/aws-smithy-types/src/config_bag.rs index 8a993e3114..12fefbe3b1 100644 --- a/rust-runtime/aws-smithy-types/src/config_bag.rs +++ b/rust-runtime/aws-smithy-types/src/config_bag.rs @@ -242,6 +242,13 @@ impl Layer { } } + pub fn with_name(self, name: impl Into>) -> Self { + Self { + name: name.into(), + props: self.props, + } + } + /// Load a storable item from the bag pub fn load(&self) -> ::ReturnedType<'_> { T::Storer::merge_iter(ItemIter {