Add support for BehaviorMajorVersions (#3151)

## Motivation and Context
See [rendered
RFC](df518bfb59/design/src/rfcs/rfc0039_behavior_major_versions.md)

## Description
This add `BehaviorMajorVersions` to the SDK and wires them in up and
down the stack.

## Testing
- [x] lots of ITs / UTs

## Checklist
<!--- If a checkbox below is not applicable, then please DELETE it
rather than leaving it unchecked -->
- [x] I have updated `CHANGELOG.next.toml` if I made changes to the
smithy-rs codegen or runtime crates
- [x] I have updated `CHANGELOG.next.toml` if I made changes to the AWS
SDK, generated SDK code, or SDK runtime crates

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
This commit is contained in:
Russell Cohen 2023-11-14 17:06:10 -05:00 committed by GitHub
parent 4128662936
commit 446326c537
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 745 additions and 409 deletions

View File

@ -67,6 +67,35 @@ references = ["smithy-rs#3160"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
author = "jdisanti"
[[aws-sdk-rust]]
message = """Clients now require a `BehaviorMajorVersion` to be provided. For must customers, `latest` is the best choice. This will be enabled automatically if you enable the `behavior-version-latest` cargo feature on `aws-config` or on an SDK crate. For customers that wish to pin to a specific behavior major version, it can be set in `aws-config` or when constructing the service client.
```rust
async fn example() {
// with aws-config
let conf = aws_config::from_env_with_version(aws_config::BehaviorMajorVersion::v2023_11_09());
// when creating a client
let client = my_service::Client::from_conf(my_service::Config::builder().behavior_major_version(..).<other params>.build());
}
```"""
references = ["smithy-rs#3151"]
author = "rcoh"
meta = { "breaking" = true, "tada" = false, "bug" = false }
[[smithy-rs]]
message = """Clients now require a `BehaviorMajorVersion` to be provided. For must customers, `latest` is the best choice. This will be enabled automatically if you enable the `behavior-version-latest` cargo feature on `aws-config` or on an SDK crate. For customers that wish to pin to a specific behavior major version, it can be set in `aws-config` or when constructing the service client.
```rust
async fn example() {
// when creating a client
let client = my_service::Client::from_conf(my_service::Config::builder().behavior_major_version(..).<other params>.build());
}
```"""
references = ["smithy-rs#3151"]
author = "rcoh"
meta = { "breaking" = true, "tada" = false, "bug" = false }
[[aws-sdk-rust]]
message = "Add `ProvideErrorMetadata` impl for service `Error` type."
references = ["aws-sdk-rust#780", "smithy-rs#3189"]

View File

@ -33,7 +33,7 @@ The SDK provides one crate per AWS service. You must add [Tokio](https://crates.
```toml
[dependencies]
aws-config = "{{sdk_version_aws_config}}"
aws-config = { version= "{{sdk_version_aws_config}}", features = ["behavior-version-latest"] }
aws-sdk-dynamodb = "{{sdk_version_aws_sdk_dynamodb}}"
tokio = { version = "1", features = ["full"] }
```

View File

@ -9,6 +9,7 @@ license = "Apache-2.0"
repository = "https://github.com/smithy-lang/smithy-rs"
[features]
behavior-version-latest = []
client-hyper = ["aws-smithy-runtime/connector-hyper-0-14-x"]
rustls = ["aws-smithy-runtime/tls-rustls", "client-hyper"]
allow-compilation = [] # our tests use `cargo test --all-features` and native-tls breaks CI
@ -71,6 +72,7 @@ serde_json = "1"
hyper-rustls = { version = "0.24", features = ["webpki-tokio", "http2", "http1"] }
aws-smithy-async = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-async", features = ["rt-tokio", "test-util"] }
[package.metadata.docs.rs]
all-features = true
targets = ["x86_64-unknown-linux-gnu"]

View File

@ -22,6 +22,7 @@ allowed_external_types = [
"aws_smithy_runtime_api::client::http::SharedHttpClient",
"aws_smithy_runtime_api::client::identity::ResolveCachedIdentity",
"aws_smithy_runtime_api::client::identity::ResolveIdentity",
"aws_smithy_runtime_api::client::behavior_version::BehaviorMajorVersion",
"aws_smithy_runtime_api::client::orchestrator::HttpResponse",
"aws_smithy_runtime_api::client::result::SdkError",
"aws_smithy_types::body::SdkBody",

View File

@ -91,6 +91,7 @@ mod tests {
use crate::profile::profile_file::{ProfileFileKind, ProfileFiles};
use crate::provider_config::ProviderConfig;
use crate::test_case::{no_traffic_client, InstantSleep};
use aws_smithy_runtime_api::client::behavior_version::BehaviorMajorVersion;
use aws_types::os_shim_internal::{Env, Fs};
#[tokio::test]
@ -117,7 +118,7 @@ mod tests {
#[tokio::test]
async fn profile_name_override() {
let fs = Fs::from_slice(&[("test_config", "[profile custom]\nsdk_ua_app_id = correct")]);
let conf = crate::from_env()
let conf = crate::from_env_with_version(BehaviorMajorVersion::latest())
.sleep_impl(InstantSleep)
.fs(fs)
.http_client(no_traffic_client())

View File

@ -11,6 +11,7 @@
rustdoc::missing_crate_level_docs,
unreachable_pub
)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
//! `aws-config` provides implementations of region and credential resolution.
//!
@ -25,14 +26,15 @@
//!
//! Load default SDK configuration:
//! ```no_run
//! # mod aws_sdk_dynamodb {
//! use aws_config::BehaviorMajorVersion;
//! mod aws_sdk_dynamodb {
//! # pub struct Client;
//! # impl Client {
//! # pub fn new(config: &aws_types::SdkConfig) -> Self { Client }
//! # }
//! # }
//! # async fn docs() {
//! let config = aws_config::load_from_env().await;
//! let config = aws_config::load_from_env_with_version(BehaviorMajorVersion::v2023_11_09()).await;
//! let client = aws_sdk_dynamodb::Client::new(&config);
//! # }
//! ```
@ -48,6 +50,7 @@
//! # async fn docs() {
//! # use aws_config::meta::region::RegionProviderChain;
//! let region_provider = RegionProviderChain::default_provider().or_else("us-east-1");
//! // Note: requires the `behavior-version-latest` feature enabled
//! let config = aws_config::from_env().region(region_provider).load().await;
//! let client = aws_sdk_dynamodb::Client::new(&config);
//! # }
@ -84,7 +87,7 @@
//! # fn custom_provider(base: &SdkConfig) -> impl ProvideCredentials {
//! # base.credentials_provider().unwrap().clone()
//! # }
//! let sdk_config = aws_config::load_from_env().await;
//! let sdk_config = aws_config::load_from_env_with_version(aws_config::BehaviorMajorVersion::latest()).await;
//! let custom_credentials_provider = custom_provider(&sdk_config);
//! let dynamo_config = aws_sdk_dynamodb::config::Builder::from(&sdk_config)
//! .credentials_provider(custom_credentials_provider)
@ -94,9 +97,11 @@
//! ```
pub use aws_smithy_http::endpoint;
pub use aws_smithy_runtime_api::client::behavior_version::BehaviorMajorVersion;
// Re-export types from aws-types
pub use aws_types::{
app_name::{AppName, InvalidAppName},
region::Region,
SdkConfig,
};
/// Load default sources for all configuration with override support
@ -137,24 +142,68 @@ pub mod web_identity_token;
/// Create an environment loader for AWS Configuration
///
/// This loader will always set [`BehaviorMajorVersion::latest`].
///
/// # Examples
/// ```no_run
/// # async fn create_config() {
/// use aws_types::region::Region;
/// let config = aws_config::from_env().region("us-east-1").load().await;
/// # }
/// ```
#[cfg(feature = "behavior-version-latest")]
pub fn from_env() -> ConfigLoader {
ConfigLoader::default()
ConfigLoader::default().behavior_major_version(BehaviorMajorVersion::latest())
}
/// Load configuration from the environment
#[cfg(not(feature = "behavior-version-latest"))]
#[deprecated(
note = "To enable the default behavior version, enable the `behavior-version-latest` feature. Alternatively, you can use [`from_env_with_version`]. This function will be removed in the next release."
)]
pub fn from_env() -> ConfigLoader {
ConfigLoader::default().behavior_major_version(BehaviorMajorVersion::latest())
}
/// Load configuration from the environment
#[cfg(not(feature = "behavior-version-latest"))]
#[deprecated(
note = "To enable the default behavior version, enable the `behavior-version-latest` feature. Alternatively, you can use [`load_from_env_with_version`]. This function will be removed in the next release."
)]
pub async fn load_from_env() -> SdkConfig {
load_from_env_with_version(BehaviorMajorVersion::latest()).await
}
/// Create an environment loader for AWS Configuration
///
/// # Examples
/// ```no_run
/// # async fn create_config() {
/// use aws_config::BehaviorMajorVersion;
/// let config = aws_config::from_env_with_version(BehaviorMajorVersion::v2023_11_09())
/// .region("us-east-1")
/// .load()
/// .await;
/// # }
/// ```
pub fn from_env_with_version(version: BehaviorMajorVersion) -> ConfigLoader {
ConfigLoader::default().behavior_major_version(version)
}
/// Load a default configuration from the environment
///
/// Convenience wrapper equivalent to `aws_config::from_env().load().await`
pub async fn load_from_env() -> aws_types::SdkConfig {
#[cfg(feature = "behavior-version-latest")]
pub async fn load_from_env() -> SdkConfig {
from_env().load().await
}
/// Load a default configuration from the environment
///
/// Convenience wrapper equivalent to `aws_config::from_env_with_version(BehaviorMajorVersion::latest()).load().await`
pub async fn load_from_env_with_version(version: BehaviorMajorVersion) -> SdkConfig {
from_env_with_version(version).load().await
}
mod loader {
use crate::default_provider::use_dual_stack::use_dual_stack_provider;
use crate::default_provider::use_fips::use_fips_provider;
@ -165,6 +214,7 @@ mod loader {
use aws_credential_types::provider::{ProvideCredentials, SharedCredentialsProvider};
use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep};
use aws_smithy_async::time::{SharedTimeSource, TimeSource};
use aws_smithy_runtime_api::client::behavior_version::BehaviorMajorVersion;
use aws_smithy_runtime_api::client::http::HttpClient;
use aws_smithy_runtime_api::client::identity::{ResolveCachedIdentity, SharedIdentityCache};
use aws_smithy_runtime_api::shared::IntoShared;
@ -212,9 +262,19 @@ mod loader {
time_source: Option<SharedTimeSource>,
env: Option<Env>,
fs: Option<Fs>,
behavior_major_version: Option<BehaviorMajorVersion>,
}
impl ConfigLoader {
/// Sets the [`BehaviorMajorVersion`] used to build [`SdkConfig`](aws_types::SdkConfig).
pub fn behavior_major_version(
mut self,
behavior_major_version: BehaviorMajorVersion,
) -> Self {
self.behavior_major_version = Some(behavior_major_version);
self
}
/// Override the region used to build [`SdkConfig`](aws_types::SdkConfig).
///
/// # Examples
@ -571,7 +631,7 @@ mod loader {
/// .enable_http1()
/// .build();
/// let provider_config = ProviderConfig::default().with_tcp_connector(custom_https_connector);
/// let shared_config = aws_config::from_env().configure(provider_config).load().await;
/// let shared_config = aws_config::from_env_with_version(BehaviorVersion::latest()).configure(provider_config).load().await;
/// # }
/// ```
#[deprecated(
@ -692,6 +752,7 @@ mod loader {
.timeout_config(timeout_config)
.time_source(time_source);
builder.set_behavior_major_version(self.behavior_major_version);
builder.set_http_client(self.http_client);
builder.set_app_name(app_name);
builder.set_identity_cache(self.identity_cache);
@ -721,7 +782,8 @@ mod loader {
mod test {
use crate::profile::profile_file::{ProfileFileKind, ProfileFiles};
use crate::test_case::{no_traffic_client, InstantSleep};
use crate::{from_env, ConfigLoader};
use crate::BehaviorMajorVersion;
use crate::{from_env_with_version, ConfigLoader};
use aws_credential_types::provider::ProvideCredentials;
use aws_smithy_async::rt::sleep::TokioSleep;
use aws_smithy_runtime::client::http::test_util::{infallible_client_fn, NeverClient};
@ -742,7 +804,7 @@ mod loader {
]);
let fs =
Fs::from_slice(&[("test_config", "[profile custom]\nsdk-ua-app-id = correct")]);
let loader = from_env()
let loader = from_env_with_version(BehaviorMajorVersion::latest())
.sleep_impl(TokioSleep::new())
.env(env)
.fs(fs)
@ -786,7 +848,7 @@ mod loader {
}
fn base_conf() -> ConfigLoader {
from_env()
from_env_with_version(BehaviorMajorVersion::latest())
.sleep_impl(InstantSleep)
.http_client(no_traffic_client())
}
@ -816,7 +878,10 @@ mod loader {
#[cfg(feature = "rustls")]
#[tokio::test]
async fn disable_default_credentials() {
let config = from_env().no_credentials().load().await;
let config = from_env_with_version(BehaviorMajorVersion::latest())
.no_credentials()
.load()
.await;
assert!(config.identity_cache().is_none());
assert!(config.credentials_provider().is_none());
}
@ -829,7 +894,7 @@ mod loader {
movable.fetch_add(1, Ordering::Relaxed);
http::Response::new("ok!")
});
let config = from_env()
let config = from_env_with_version(BehaviorMajorVersion::latest())
.fs(Fs::from_slice(&[]))
.env(Env::from_slice(&[]))
.http_client(http_client.clone())

View File

@ -196,7 +196,8 @@ impl ProviderConfig {
.region(self.region())
.time_source(self.time_source())
.use_fips(self.use_fips().unwrap_or_default())
.use_dual_stack(self.use_dual_stack().unwrap_or_default());
.use_dual_stack(self.use_dual_stack().unwrap_or_default())
.behavior_major_version(crate::BehaviorMajorVersion::latest());
builder.set_http_client(self.http_client.clone());
builder.set_sleep_impl(self.sleep_impl.clone());
builder.build()

View File

@ -319,7 +319,9 @@ impl Builder {
/// This will panic if any of the required fields are not given.
pub async fn build(mut self) -> SsoTokenProvider {
if self.sdk_config.is_none() {
self.sdk_config = Some(crate::load_from_env().await);
self.sdk_config = Some(
crate::load_from_env_with_version(crate::BehaviorMajorVersion::latest()).await,
);
}
self.build_with(Env::real(), Fs::real())
}
@ -427,6 +429,7 @@ mod tests {
.sleep_impl(SharedAsyncSleep::new(sleep_impl))
// disable retry to simplify testing
.retry_config(RetryConfig::disabled())
.behavior_major_version(crate::BehaviorMajorVersion::latest())
.build();
Self {
time_source,

View File

@ -221,7 +221,7 @@ impl AssumeRoleProviderBuilder {
pub async fn build(self) -> AssumeRoleProvider {
let mut conf = match self.sdk_config {
Some(conf) => conf,
None => crate::load_from_env().await,
None => crate::load_from_env_with_version(crate::BehaviorMajorVersion::latest()).await,
};
// ignore a identity cache set from SdkConfig
conf = conf
@ -264,7 +264,7 @@ impl AssumeRoleProviderBuilder {
) -> AssumeRoleProvider {
let conf = match self.sdk_config {
Some(conf) => conf,
None => crate::load_from_env().await,
None => crate::load_from_env_with_version(crate::BehaviorMajorVersion::latest()).await,
};
let conf = conf
.into_builder()
@ -334,6 +334,7 @@ mod test {
capture_request, ReplayEvent, StaticReplayClient,
};
use aws_smithy_runtime::test_util::capture_test_logs::capture_test_logs;
use aws_smithy_runtime_api::client::behavior_version::BehaviorMajorVersion;
use aws_smithy_types::body::SdkBody;
use aws_types::os_shim_internal::Env;
use aws_types::region::Region;
@ -351,6 +352,7 @@ mod test {
))
.http_client(http_client)
.region(Region::from_static("this-will-be-overridden"))
.behavior_major_version(crate::BehaviorMajorVersion::latest())
.build();
let provider = AssumeRoleProvider::builder("myrole")
.configure(&sdk_config)
@ -371,6 +373,7 @@ mod test {
async fn loads_region_from_sdk_config() {
let (http_client, request) = capture_request(None);
let sdk_config = SdkConfig::builder()
.behavior_major_version(crate::BehaviorMajorVersion::latest())
.sleep_impl(SharedAsyncSleep::new(TokioSleep::new()))
.time_source(StaticTimeSource::new(
UNIX_EPOCH + Duration::from_secs(1234567890 - 120),
@ -405,7 +408,7 @@ mod test {
.body(SdkBody::from(""))
.unwrap(),
));
let conf = crate::from_env()
let conf = crate::from_env_with_version(BehaviorMajorVersion::latest())
.env(Env::from_slice(&[
("AWS_ACCESS_KEY_ID", "123-key"),
("AWS_SECRET_ACCESS_KEY", "456"),
@ -421,7 +424,7 @@ mod test {
.configure(&conf)
.build()
.await;
let _ = provider.provide_credentials().await;
let _ = dbg!(provider.provide_credentials().await);
let req = request.expect_request();
let auth_header = req.headers().get(AUTHORIZATION).unwrap().to_string();
let expect = "Credential=123-key/20090213/us-west-17/sts/aws4_request";
@ -454,6 +457,7 @@ mod test {
.sleep_impl(SharedAsyncSleep::new(sleep))
.time_source(testing_time_source.clone())
.http_client(http_client)
.behavior_major_version(crate::BehaviorMajorVersion::latest())
.build();
let credentials_list = std::sync::Arc::new(std::sync::Mutex::new(vec![
Credentials::new(

View File

@ -9,6 +9,7 @@ allowed_external_types = [
"aws_smithy_runtime_api::client::http::SharedHttpClient",
"aws_smithy_runtime_api::client::identity::ResolveCachedIdentity",
"aws_smithy_runtime_api::client::identity::SharedIdentityCache",
"aws_smithy_runtime_api::client::behavior_version::BehaviorMajorVersion",
"aws_smithy_runtime_api::http::headers::Headers",
"aws_smithy_types::config_bag::storable::Storable",
"aws_smithy_types::config_bag::storable::StoreReplace",

View File

@ -17,6 +17,7 @@ pub use aws_credential_types::provider::SharedCredentialsProvider;
use aws_smithy_async::rt::sleep::AsyncSleep;
pub use aws_smithy_async::rt::sleep::SharedAsyncSleep;
pub use aws_smithy_async::time::{SharedTimeSource, TimeSource};
use aws_smithy_runtime_api::client::behavior_version::BehaviorMajorVersion;
use aws_smithy_runtime_api::client::http::HttpClient;
pub use aws_smithy_runtime_api::client::http::SharedHttpClient;
use aws_smithy_runtime_api::client::identity::{ResolveCachedIdentity, SharedIdentityCache};
@ -62,6 +63,7 @@ pub struct SdkConfig {
http_client: Option<SharedHttpClient>,
use_fips: Option<bool>,
use_dual_stack: Option<bool>,
behavior_major_version: Option<BehaviorMajorVersion>,
}
/// Builder for AWS Shared Configuration
@ -83,6 +85,7 @@ pub struct Builder {
http_client: Option<SharedHttpClient>,
use_fips: Option<bool>,
use_dual_stack: Option<bool>,
behavior_major_version: Option<BehaviorMajorVersion>,
}
impl Builder {
@ -536,6 +539,21 @@ impl Builder {
self
}
/// Sets the [`BehaviorMajorVersion`] for the [`SdkConfig`]
pub fn behavior_major_version(mut self, behavior_major_version: BehaviorMajorVersion) -> Self {
self.set_behavior_major_version(Some(behavior_major_version));
self
}
/// Sets the [`BehaviorMajorVersion`] for the [`SdkConfig`]
pub fn set_behavior_major_version(
&mut self,
behavior_major_version: Option<BehaviorMajorVersion>,
) -> &mut Self {
self.behavior_major_version = behavior_major_version;
self
}
/// Build a [`SdkConfig`](SdkConfig) from this builder
pub fn build(self) -> SdkConfig {
SdkConfig {
@ -551,6 +569,7 @@ impl Builder {
use_fips: self.use_fips,
use_dual_stack: self.use_dual_stack,
time_source: self.time_source,
behavior_major_version: self.behavior_major_version,
}
}
}
@ -617,6 +636,11 @@ impl SdkConfig {
self.use_dual_stack
}
/// Behavior major version configured for this client
pub fn behavior_major_version(&self) -> Option<BehaviorMajorVersion> {
self.behavior_major_version.clone()
}
/// Config builder
///
/// _Important:_ Using the `aws-config` crate to configure the SDK is preferred to invoking this
@ -646,6 +670,7 @@ impl SdkConfig {
http_client: self.http_client,
use_fips: self.use_fips,
use_dual_stack: self.use_dual_stack,
behavior_major_version: self.behavior_major_version,
}
}
}

View File

@ -148,7 +148,7 @@ internal class AwsCrateDocGenerator(private val codegenContext: ClientCodegenCon
```toml
[dependencies]
aws-config = "$awsConfigVersion"
aws-config = { version = "$awsConfigVersion", features = ["behavior-version-latest"] }
$moduleName = "${codegenContext.settings.moduleVersion}"
tokio = { version = "1", features = ["full"] }
```

View File

@ -117,6 +117,8 @@ private class AwsFluentClientExtensions(private val codegenContext: ClientCodege
/// the `sleep_impl` on the Config passed into this function to fix it.
/// - This method will panic if the `sdk_config` is missing an HTTP connector. If you experience this panic, set the
/// `http_connector` on the Config passed into this function to fix it.
/// - This method will panic if no `BehaviorMajorVersion` is provided. If you experience this panic, set `behavior_major_version` on the Config or enable the `behavior-version-latest` Cargo feature.
##[track_caller]
pub fn new(sdk_config: &#{aws_types}::sdk_config::SdkConfig) -> Self {
Self::from_conf(sdk_config.into())
}

View File

@ -78,6 +78,7 @@ class GenericSmithySdkConfigSettings : ClientCodegenDecorator {
${section.serviceConfigBuilder}.set_http_client(${section.sdkConfig}.http_client());
${section.serviceConfigBuilder}.set_time_source(${section.sdkConfig}.time_source());
${section.serviceConfigBuilder}.set_behavior_major_version(${section.sdkConfig}.behavior_major_version());
if let Some(cache) = ${section.sdkConfig}.identity_cache() {
${section.serviceConfigBuilder}.set_identity_cache(cache);

View File

@ -5,20 +5,14 @@
package software.amazon.smithy.rustsdk
import SdkCodegenIntegrationTest
import org.junit.jupiter.api.Test
import software.amazon.smithy.rust.codegen.client.testutil.validateConfigCustomizations
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.integrationTest
import software.amazon.smithy.rust.codegen.core.testutil.tokioTest
internal class CredentialProviderConfigTest {
@Test
fun `generates a valid config`() {
val codegenContext = awsTestCodegenContext()
validateConfigCustomizations(codegenContext, CredentialProviderConfig(codegenContext))
}
@Test
fun `configuring credentials provider at operation level should work`() {
awsSdkIntegrationTest(SdkCodegenIntegrationTest.model) { ctx, rustCrate ->

View File

@ -5,22 +5,18 @@
package software.amazon.smithy.rustsdk
import SdkCodegenIntegrationTest
import org.junit.jupiter.api.Test
import software.amazon.smithy.rust.codegen.client.testutil.testClientRustSettings
import software.amazon.smithy.rust.codegen.client.testutil.validateConfigCustomizations
import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace
import software.amazon.smithy.rust.codegen.core.testutil.rustSettings
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.core.testutil.unitTest
internal class RegionProviderConfigTest {
@Test
fun `generates a valid config`() {
val project = TestWorkspace.testProject()
val codegenContext = awsTestCodegenContext(
settings = testClientRustSettings(
moduleName = project.rustSettings().moduleName,
runtimeConfig = AwsTestRuntimeConfig,
),
)
validateConfigCustomizations(codegenContext, RegionProviderConfig(codegenContext), project)
awsSdkIntegrationTest(SdkCodegenIntegrationTest.model) { _ctx, crate ->
crate.unitTest {
rustTemplate("let conf: Option<crate::Config> = None; let _reg: Option<crate::config::Region> = conf.and_then(|c|c.region().cloned());")
}
}
}
}

View File

@ -41,27 +41,29 @@ fun awsSdkIntegrationTest(
) =
clientIntegrationTest(
model,
IntegrationTestParams(
cargoCommand = "cargo test --features test-util",
runtimeConfig = AwsTestRuntimeConfig,
additionalSettings = ObjectNode.builder().withMember(
"customizationConfig",
ObjectNode.builder()
.withMember(
"awsSdk",
ObjectNode.builder()
.withMember("generateReadme", false)
.withMember("integrationTestPath", "../sdk/integration-tests")
.build(),
).build(),
)
.withMember(
"codegen",
ObjectNode.builder()
.withMember("includeFluentClient", false)
.withMember("includeEndpointUrlConfig", false)
.build(),
).build(),
),
awsIntegrationTestParams(),
test = test,
)
fun awsIntegrationTestParams() = IntegrationTestParams(
cargoCommand = "cargo test --features test-util behavior-version-latest",
runtimeConfig = AwsTestRuntimeConfig,
additionalSettings = ObjectNode.builder().withMember(
"customizationConfig",
ObjectNode.builder()
.withMember(
"awsSdk",
ObjectNode.builder()
.withMember("generateReadme", false)
.withMember("integrationTestPath", "../sdk/integration-tests")
.build(),
).build(),
)
.withMember(
"codegen",
ObjectNode.builder()
.withMember("includeFluentClient", false)
.withMember("includeEndpointUrlConfig", false)
.build(),
).build(),
)

View File

@ -15,7 +15,7 @@ approx = "0.5.1"
aws-config = { path = "../../build/aws-sdk/sdk/aws-config" }
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../build/aws-sdk/sdk/aws-http" }
aws-sdk-dynamodb = { path = "../../build/aws-sdk/sdk/dynamodb" }
aws-sdk-dynamodb = { path = "../../build/aws-sdk/sdk/dynamodb", features = ["behavior-version-latest"] }
aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async", features = ["test-util"] }
aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" }
aws-smithy-protocol-test = { path = "../../build/aws-sdk/sdk/aws-smithy-protocol-test" }

View File

@ -12,7 +12,7 @@ aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async" }
aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] }
aws-smithy-runtime-api = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime-api", features = ["client"] }
aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" }
aws-sdk-ec2 = { path = "../../build/aws-sdk/sdk/ec2" }
aws-sdk-ec2 = { path = "../../build/aws-sdk/sdk/ec2", features = ["behavior-version-latest"] }
tokio = { version = "1.23.1", features = ["full"]}
http = "0.2.0"
tokio-stream = "0.1.5"

View File

@ -13,7 +13,7 @@ publish = false
[dev-dependencies]
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../build/aws-sdk/sdk/aws-http"}
aws-sdk-glacier = { path = "../../build/aws-sdk/sdk/glacier" }
aws-sdk-glacier = { path = "../../build/aws-sdk/sdk/glacier", features = ["behavior-version-latest"] }
aws-smithy-protocol-test = { path = "../../build/aws-sdk/sdk/aws-smithy-protocol-test"}
aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] }
bytes = "1.0.0"

View File

@ -13,7 +13,7 @@ publish = false
[dev-dependencies]
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../build/aws-sdk/sdk/aws-http"}
aws-sdk-iam = { path = "../../build/aws-sdk/sdk/iam" }
aws-sdk-iam = { path = "../../build/aws-sdk/sdk/iam", features = ["behavior-version-latest"] }
aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] }
aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" }
bytes = "1.0.0"

View File

@ -6,19 +6,17 @@
use aws_sdk_iam::config::{Credentials, Region};
use aws_smithy_runtime::client::http::test_util::capture_request;
// this test is ignored because pseudoregions have been removed. This test should be re-enabled
// once FIPS support is added in aws-config
#[tokio::test]
#[ignore]
async fn correct_endpoint_resolver() {
let (http_client, request) = capture_request(None);
let conf = aws_sdk_iam::Config::builder()
.region(Region::from_static("iam-fips"))
.credentials_provider(Credentials::for_tests())
.use_fips(true)
.region(Region::new("us-east-1"))
.http_client(http_client)
.build();
let client = aws_sdk_iam::Client::from_conf(conf);
let _ = client.list_roles().send().await;
let _ = dbg!(client.list_roles().send().await);
let req = request.expect_request();
assert_eq!(&req.uri().to_string(), "https://iam-fips.amazonaws.com/");
}

View File

@ -18,7 +18,7 @@ test-util = []
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../build/aws-sdk/sdk/aws-http" }
aws-runtime = { path = "../../build/aws-sdk/sdk/aws-runtime" }
aws-sdk-kms = { path = "../../build/aws-sdk/sdk/kms", features = ["test-util"] }
aws-sdk-kms = { path = "../../build/aws-sdk/sdk/kms", features = ["test-util", "behavior-version-latest"] }
aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async", features = ["test-util"] }
aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" }
aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" }

View File

@ -11,7 +11,7 @@ publish = false
async-stream = "0.3.0"
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../build/aws-sdk/sdk/aws-http" }
aws-sdk-lambda = { path = "../../build/aws-sdk/sdk/lambda" }
aws-sdk-lambda = { path = "../../build/aws-sdk/sdk/lambda", features = ["behavior-version-latest"] }
aws-smithy-eventstream = { path = "../../build/aws-sdk/sdk/aws-smithy-eventstream" }
aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" }
aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] }

View File

@ -22,3 +22,4 @@ aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types",
futures = "0.3.25"
tokio = { version = "1.23.1", features = ["full", "test-util"] }
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
http = "0.2.9"

View File

@ -4,10 +4,14 @@
*/
use aws_sdk_s3::config::IdentityCache;
use aws_sdk_s3::config::{
retry::RetryConfig, timeout::TimeoutConfig, Config, Credentials, Region, SharedAsyncSleep,
Sleep,
retry::RetryConfig, timeout::TimeoutConfig, BehaviorMajorVersion, Config, Credentials, Region,
SharedAsyncSleep, Sleep,
};
use aws_sdk_s3::primitives::SdkBody;
use aws_smithy_runtime::client::http::test_util::infallible_client_fn;
use aws_sdk_s3::error::DisplayErrorContext;
use aws_smithy_async::rt::sleep::AsyncSleep;
use aws_smithy_runtime::client::http::test_util::capture_request;
@ -22,7 +26,7 @@ use std::time::Duration;
expected = "Enable the `rustls` crate feature or configure a HTTP client to fix this."
)]
async fn test_clients_from_sdk_config() {
aws_config::load_from_env().await;
aws_config::load_from_env_with_version(BehaviorMajorVersion::latest()).await;
}
// This will fail due to lack of a connector when constructing the service client
@ -42,6 +46,7 @@ async fn test_clients_from_service_config() {
.region(Region::new("us-east-1"))
.credentials_provider(Credentials::for_tests())
.sleep_impl(SharedAsyncSleep::new(StubSleep))
.behavior_major_version(BehaviorMajorVersion::latest())
.build();
// Creating the client shouldn't panic or error since presigning doesn't require a connector
let client = aws_sdk_s3::Client::from_conf(config);
@ -58,6 +63,23 @@ async fn test_clients_from_service_config() {
);
}
#[tokio::test]
#[should_panic(expected = "Invalid client configuration: A behavior major version must be set")]
async fn test_missing_behavior_major_version() {
use aws_sdk_s3::config::Region;
let http_client =
infallible_client_fn(|_req| http::Response::builder().body(SdkBody::empty()).unwrap());
let config = Config::builder()
.region(Region::new("us-east-1"))
.identity_cache(IdentityCache::no_cache())
.credentials_provider(Credentials::for_tests())
.http_client(http_client)
.build();
// This line panics
let _client = aws_sdk_s3::Client::from_conf(config);
}
#[tokio::test]
#[should_panic(
expected = "Invalid client configuration: An async sleep implementation is required for retry to work."
@ -73,6 +95,7 @@ async fn test_missing_async_sleep_time_source_retries() {
.credentials_provider(Credentials::for_tests())
.retry_config(RetryConfig::standard())
.timeout_config(TimeoutConfig::disabled())
.behavior_major_version(BehaviorMajorVersion::latest())
.build();
// should panic with a validation error
@ -93,6 +116,7 @@ async fn test_missing_async_sleep_time_source_timeouts() {
.region(Region::new("us-east-1"))
.credentials_provider(Credentials::for_tests())
.retry_config(RetryConfig::disabled())
.behavior_major_version(BehaviorMajorVersion::latest())
.timeout_config(
TimeoutConfig::builder()
.operation_timeout(Duration::from_secs(5))
@ -120,8 +144,60 @@ async fn test_time_source_for_identity_cache() {
.credentials_provider(Credentials::for_tests())
.retry_config(RetryConfig::disabled())
.timeout_config(TimeoutConfig::disabled())
.behavior_major_version(BehaviorMajorVersion::latest())
.build();
// should panic with a validation error
let _client = aws_sdk_s3::Client::from_conf(config);
}
#[tokio::test]
async fn behavior_mv_from_aws_config() {
let (http_client, req) = capture_request(None);
let cfg = aws_config::from_env_with_version(BehaviorMajorVersion::v2023_11_09())
.http_client(http_client)
.retry_config(RetryConfig::disabled())
.credentials_provider(Credentials::for_tests())
.identity_cache(IdentityCache::no_cache())
.timeout_config(TimeoutConfig::disabled())
.region(Region::new("us-west-2"))
.load()
.await;
let s3_client = aws_sdk_s3::Client::new(&cfg);
let _err = s3_client
.list_buckets()
.send()
.await
.expect_err("it should fail to send a request because there is no HTTP client");
assert_eq!(
req.expect_request().uri(),
"https://s3.us-west-2.amazonaws.com/"
);
}
#[tokio::test]
async fn behavior_mv_from_client_construction() {
let (http_client, req) = capture_request(None);
let cfg = aws_config::SdkConfig::builder()
.http_client(http_client)
.retry_config(RetryConfig::disabled())
.identity_cache(IdentityCache::no_cache())
.timeout_config(TimeoutConfig::disabled())
.region(Region::new("us-west-2"))
.build();
let s3_client = aws_sdk_s3::Client::from_conf(
aws_sdk_s3::config::Builder::from(&cfg)
.credentials_provider(Credentials::for_tests())
.behavior_major_version(aws_sdk_s3::config::BehaviorMajorVersion::v2023_11_09())
.build(),
);
let _err = dbg!(s3_client
.list_buckets()
.send()
.await
.expect_err("it should fail to send a request because there is no HTTP client"));
assert_eq!(
req.expect_request().uri(),
"https://s3.us-west-2.amazonaws.com/"
);
}

View File

@ -13,7 +13,7 @@ publish = false
[dev-dependencies]
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../build/aws-sdk/sdk/aws-http"}
aws-sdk-polly = { path = "../../build/aws-sdk/sdk/polly" }
aws-sdk-polly = { path = "../../build/aws-sdk/sdk/polly", features = ["behavior-version-latest"] }
aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" }
bytes = "1.0.0"
http = "0.2.0"

View File

@ -17,7 +17,7 @@ test-util = []
[dev-dependencies]
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../build/aws-sdk/sdk/aws-http" }
aws-sdk-qldbsession = { path = "../../build/aws-sdk/sdk/qldbsession", features = ["test-util"] }
aws-sdk-qldbsession = { path = "../../build/aws-sdk/sdk/qldbsession", features = ["test-util", "behavior-version-latest"] }
aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async" }
aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" }
aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] }

View File

@ -16,12 +16,12 @@ test-util = []
[dev-dependencies]
async-std = "1.12.0"
aws-config = { path = "../../build/aws-sdk/sdk/aws-config" }
aws-config = { path = "../../build/aws-sdk/sdk/aws-config", features = ["behavior-version-latest"] }
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../build/aws-sdk/sdk/aws-http" }
aws-runtime = { path = "../../build/aws-sdk/sdk/aws-runtime", features = ["test-util"] }
aws-sdk-s3 = { path = "../../build/aws-sdk/sdk/s3", features = ["test-util"] }
aws-sdk-sts = { path = "../../build/aws-sdk/sdk/sts" }
aws-sdk-s3 = { path = "../../build/aws-sdk/sdk/s3", features = ["test-util", "behavior-version-latest"] }
# aws-sdk-sts = { path = "../../build/aws-sdk/sdk/sts" }
aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async", features = ["test-util", "rt-tokio"] }
aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" }
aws-smithy-protocol-test = { path = "../../build/aws-sdk/sdk/aws-smithy-protocol-test" }
@ -32,7 +32,7 @@ aws-types = { path = "../../build/aws-sdk/sdk/aws-types" }
bytes = "1"
bytes-utils = "0.1.2"
fastrand = "2.0.1"
futures-util = { version = "0.3.16" }
futures-util = { version = "0.3.16", default-features = false, features = ["alloc"] }
hdrhistogram = "7.5.2"
http = "0.2.3"
http-body = "0.4.5"

View File

@ -17,6 +17,7 @@ fn test_client(update_builder: fn(Builder) -> Builder) -> (CaptureRequestReceive
.credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests()))
.region(Region::new("us-west-4"))
.http_client(http_client)
.behavior_major_version(aws_sdk_s3::config::BehaviorMajorVersion::latest())
.with_test_defaults();
let client = Client::from_conf(update_builder(config).build());
(captured_request, client)

View File

@ -17,7 +17,7 @@ test-util = []
[dev-dependencies]
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../build/aws-sdk/sdk/aws-http" }
aws-sdk-s3control = { path = "../../build/aws-sdk/sdk/s3control", features = ["test-util"] }
aws-sdk-s3control = { path = "../../build/aws-sdk/sdk/s3control", features = ["test-util", "behavior-version-latest"] }
aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async" }
aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] }
aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" }

View File

@ -12,7 +12,7 @@ publish = false
[dev-dependencies]
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-sdk-sts = { path = "../../build/aws-sdk/sdk/sts" }
aws-sdk-sts = { path = "../../build/aws-sdk/sdk/sts", features = ["behavior-version-latest"] }
aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" }
aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] }
aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" }

View File

@ -11,6 +11,6 @@ for f in *; do
echo
echo "Testing ${f}..."
echo "###############"
cargo test --manifest-path "${f}/Cargo.toml"
cargo test --manifest-path "${f}/Cargo.toml" --all-features
fi
done

View File

@ -12,7 +12,7 @@ publish = false
[dev-dependencies]
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-sdk-timestreamquery = { path = "../../build/aws-sdk/sdk/timestreamquery" }
aws-sdk-timestreamquery = { path = "../../build/aws-sdk/sdk/timestreamquery", features = ["behavior-version-latest"] }
aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async", features = ["test-util"] }
aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["test-util"] }
aws-types = { path = "../../build/aws-sdk/sdk/aws-types" }

View File

@ -12,7 +12,7 @@ publish = false
async-stream = "0.3.0"
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../build/aws-sdk/sdk/aws-http" }
aws-sdk-transcribestreaming = { path = "../../build/aws-sdk/sdk/transcribestreaming" }
aws-sdk-transcribestreaming = { path = "../../build/aws-sdk/sdk/transcribestreaming", features = ["behavior-version-latest"] }
aws-smithy-eventstream = { path = "../../build/aws-sdk/sdk/aws-smithy-eventstream" }
aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" }
aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] }

View File

@ -18,7 +18,7 @@ crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
aws-config = { path = "../../build/aws-sdk/sdk/aws-config", default-features = false, features = ["rt-tokio"]}
aws-config = { path = "../../build/aws-sdk/sdk/aws-config", default-features = false, features = ["rt-tokio", "behavior-version-latest"]}
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["hardcoded-credentials"] }
aws-sdk-s3 = { path = "../../build/aws-sdk/sdk/s3", default-features = false }
aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" }

View File

@ -36,6 +36,7 @@ internal class EndpointConfigCustomization(
"SharedEndpointResolver" to epModule.resolve("SharedEndpointResolver"),
"StaticUriEndpointResolver" to epRuntimeModule.resolve("StaticUriEndpointResolver"),
"ServiceSpecificResolver" to codegenContext.serviceSpecificEndpointResolver(),
"IntoShared" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("shared::IntoShared"),
)
override fun section(section: ServiceConfig): Writable {
@ -89,7 +90,7 @@ internal class EndpointConfigCustomization(
##[allow(deprecated)]
self.set_endpoint_resolver(
endpoint_url.map(|url| {
#{StaticUriEndpointResolver}::uri(url).into_shared()
#{IntoShared}::into_shared(#{StaticUriEndpointResolver}::uri(url))
})
);
self

View File

@ -20,6 +20,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.derive
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.core.rustlang.EscapeFor
import software.amazon.smithy.rust.codegen.core.rustlang.Feature
import software.amazon.smithy.rust.codegen.core.rustlang.RustModule
import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords
import software.amazon.smithy.rust.codegen.core.rustlang.RustType
@ -31,6 +32,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.docLink
import software.amazon.smithy.rust.codegen.core.rustlang.docs
import software.amazon.smithy.rust.codegen.core.rustlang.documentShape
import software.amazon.smithy.rust.codegen.core.rustlang.escape
import software.amazon.smithy.rust.codegen.core.rustlang.featureGatedBlock
import software.amazon.smithy.rust.codegen.core.rustlang.implBlock
import software.amazon.smithy.rust.codegen.core.rustlang.normalizeHtml
import software.amazon.smithy.rust.codegen.core.rustlang.qualifiedName
@ -57,10 +59,12 @@ import software.amazon.smithy.rust.codegen.core.util.outputShape
import software.amazon.smithy.rust.codegen.core.util.sdkId
import software.amazon.smithy.rust.codegen.core.util.toSnakeCase
private val BehaviorVersionLatest = Feature("behavior-version-latest", false, listOf())
class FluentClientGenerator(
private val codegenContext: ClientCodegenContext,
private val customizations: List<FluentClientCustomization> = emptyList(),
) {
companion object {
fun clientOperationFnName(operationShape: OperationShape, symbolProvider: RustSymbolProvider): String =
RustReservedWords.escapeIfNeeded(symbolProvider.toSymbol(operationShape).name.toSnakeCase())
@ -94,6 +98,7 @@ class FluentClientGenerator(
}
private fun renderFluentClient(crate: RustCrate) {
crate.mergeFeature(BehaviorVersionLatest)
crate.withModule(ClientRustModule.client) {
rustTemplate(
"""
@ -119,8 +124,10 @@ class FluentClientGenerator(
///
/// - Retries or timeouts are enabled without a `sleep_impl` configured.
/// - Identity caching is enabled without a `sleep_impl` and `time_source` configured.
/// - No `behavior_major_version` is provided.
///
/// The panic message for each of these will have instructions on how to resolve them.
##[track_caller]
pub fn from_conf(conf: crate::Config) -> Self {
let handle = Handle {
conf: conf.clone(),
@ -451,6 +458,9 @@ private fun baseClientRuntimePluginsFn(codegenContext: ClientCodegenContext): Ru
RuntimeType.forInlineFun("base_client_runtime_plugins", ClientRustModule.config) {
val api = RuntimeType.smithyRuntimeApiClient(rc)
val rt = RuntimeType.smithyRuntime(rc)
val behaviorVersionError = "Invalid client configuration: A behavior major version must be set when sending a " +
"request or constructing a client. You must set it during client construction or by enabling the " +
"`${BehaviorVersionLatest.name}` cargo feature."
rustTemplate(
"""
pub(crate) fn base_client_runtime_plugins(
@ -458,12 +468,16 @@ private fun baseClientRuntimePluginsFn(codegenContext: ClientCodegenContext): Ru
) -> #{RuntimePlugins} {
let mut configured_plugins = #{Vec}::new();
::std::mem::swap(&mut config.runtime_plugins, &mut configured_plugins);
##[allow(unused_mut)]
let mut behavior_major_version = config.behavior_major_version.clone();
#{update_bmv}
let mut plugins = #{RuntimePlugins}::new()
// defaults
.with_client_plugins(#{default_plugins}(
#{DefaultPluginParams}::new()
.with_retry_partition_name(${codegenContext.serviceShape.sdkId().dq()})
.with_behavior_major_version(behavior_major_version.expect(${behaviorVersionError.dq()}))
))
// user config
.with_client_plugin(
@ -474,6 +488,7 @@ private fun baseClientRuntimePluginsFn(codegenContext: ClientCodegenContext): Ru
// codegen config
.with_client_plugin(crate::config::ServiceRuntimePlugin::new(config))
.with_client_plugin(#{NoAuthRuntimePlugin}::new());
for plugin in configured_plugins {
plugins = plugins.with_client_plugin(plugin);
}
@ -486,6 +501,17 @@ private fun baseClientRuntimePluginsFn(codegenContext: ClientCodegenContext): Ru
"NoAuthRuntimePlugin" to rt.resolve("client::auth::no_auth::NoAuthRuntimePlugin"),
"RuntimePlugins" to RuntimeType.runtimePlugins(rc),
"StaticRuntimePlugin" to api.resolve("client::runtime_plugin::StaticRuntimePlugin"),
"update_bmv" to featureGatedBlock(BehaviorVersionLatest) {
rustTemplate(
"""
if behavior_major_version.is_none() {
behavior_major_version = Some(#{BehaviorMajorVersion}::latest());
}
""",
"BehaviorMajorVersion" to api.resolve("client::behavior_version::BehaviorMajorVersion"),
)
},
)
}
}

View File

@ -189,37 +189,38 @@ fun loadFromConfigBag(innerTypeName: String, newtype: RuntimeType): Writable = w
* 2. convenience setter (non-optional)
* 3. standard setter (&mut self)
*/
fun standardConfigParam(param: ConfigParam, codegenContext: ClientCodegenContext): ConfigCustomization = object : ConfigCustomization() {
override fun section(section: ServiceConfig): Writable {
return when (section) {
ServiceConfig.BuilderImpl -> writable {
docsOrFallback(param.setterDocs)
rust(
"""
pub fn ${param.name}(mut self, ${param.name}: impl Into<#T>) -> Self {
self.set_${param.name}(Some(${param.name}.into()));
self
fun standardConfigParam(param: ConfigParam, codegenContext: ClientCodegenContext): ConfigCustomization =
object : ConfigCustomization() {
override fun section(section: ServiceConfig): Writable {
return when (section) {
ServiceConfig.BuilderImpl -> writable {
docsOrFallback(param.setterDocs)
rust(
"""
pub fn ${param.name}(mut self, ${param.name}: impl Into<#T>) -> Self {
self.set_${param.name}(Some(${param.name}.into()));
self
}""",
param.type,
)
param.type,
)
docsOrFallback(param.setterDocs)
rustTemplate(
"""
pub fn set_${param.name}(&mut self, ${param.name}: Option<#{T}>) -> &mut Self {
self.config.store_or_unset(${param.name}.map(#{newtype}));
self
}
""",
"T" to param.type,
"newtype" to param.newtype!!,
)
docsOrFallback(param.setterDocs)
rustTemplate(
"""
pub fn set_${param.name}(&mut self, ${param.name}: Option<#{T}>) -> &mut Self {
self.config.store_or_unset(${param.name}.map(#{newtype}));
self
}
""",
"T" to param.type,
"newtype" to param.newtype!!,
)
}
else -> emptySection
}
else -> emptySection
}
}
}
fun ServiceShape.needsIdempotencyToken(model: Model): Boolean {
val operationIndex = OperationIndex.of(model)
@ -279,8 +280,69 @@ class ServiceConfigGenerator(
"RuntimePlugin" to configReexport(RuntimeType.runtimePlugin(runtimeConfig)),
"SharedRuntimePlugin" to configReexport(RuntimeType.sharedRuntimePlugin(runtimeConfig)),
"runtime_plugin" to RuntimeType.smithyRuntimeApiClient(runtimeConfig).resolve("client::runtime_plugin"),
"BehaviorMajorVersion" to configReexport(
RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("client::behavior_version::BehaviorMajorVersion"),
),
)
private fun behaviorMv() = writable {
val docs = """
/// Sets the [`behavior major version`](crate::config::BehaviorMajorVersion).
///
/// Over time, new best-practice behaviors are introduced. However, these behaviors might not be backwards
/// compatible. For example, a change which introduces new default timeouts or a new retry-mode for
/// all operations might be the ideal behavior but could break existing applications.
///
/// ## Examples
///
/// Set the behavior major version to `latest`. This is equivalent to enabling the `behavior-version-latest` cargo feature.
/// ```no_run
/// use $moduleUseName::config::BehaviorMajorVersion;
///
/// let config = $moduleUseName::Config::builder()
/// .behavior_major_version(BehaviorMajorVersion::latest())
/// // ...
/// .build();
/// let client = $moduleUseName::Client::from_conf(config);
/// ```
///
/// Customizing behavior major version:
/// ```no_run
/// use $moduleUseName::config::BehaviorMajorVersion;
///
/// let config = $moduleUseName::Config::builder()
/// .behavior_major_version(BehaviorMajorVersion::v2023_11_09())
/// // ...
/// .build();
/// let client = $moduleUseName::Client::from_conf(config);
/// ```
"""
rustTemplate(
"""
$docs
pub fn behavior_major_version(mut self, behavior_major_version: crate::config::BehaviorMajorVersion) -> Self {
self.set_behavior_major_version(Some(behavior_major_version));
self
}
$docs
pub fn set_behavior_major_version(&mut self, behavior_major_version: Option<crate::config::BehaviorMajorVersion>) -> &mut Self {
self.behavior_major_version = behavior_major_version;
self
}
/// Convenience method to set the latest behavior major version
///
/// This is equivalent to enabling the `behavior-version-latest` Cargo feature
pub fn behavior_major_version_latest(mut self) -> Self {
self.set_behavior_major_version(Some(crate::config::BehaviorMajorVersion::latest()));
self
}
""",
*codegenScope,
)
}
fun render(writer: RustWriter) {
writer.docs("Configuration for a $moduleUseName service client.\n")
customizations.forEach {
@ -297,6 +359,7 @@ class ServiceConfigGenerator(
cloneable: #{CloneableLayer},
pub(crate) runtime_components: #{RuntimeComponentsBuilder},
pub(crate) runtime_plugins: #{Vec}<#{SharedRuntimePlugin}>,
behavior_major_version: #{Option}<#{BehaviorMajorVersion}>,
""",
*codegenScope,
)
@ -320,6 +383,7 @@ class ServiceConfigGenerator(
config: self.cloneable.clone(),
runtime_components: self.runtime_components.clone(),
runtime_plugins: self.runtime_plugins.clone(),
behavior_major_version: self.behavior_major_version.clone(),
}
}
""",
@ -337,6 +401,7 @@ class ServiceConfigGenerator(
pub(crate) config: #{CloneableLayer},
pub(crate) runtime_components: #{RuntimeComponentsBuilder},
pub(crate) runtime_plugins: #{Vec}<#{SharedRuntimePlugin}>,
pub(crate) behavior_major_version: #{Option}<#{BehaviorMajorVersion}>,
""",
*codegenScope,
)
@ -354,6 +419,7 @@ class ServiceConfigGenerator(
config: #{Default}::default(),
runtime_components: #{RuntimeComponentsBuilder}::new("service config"),
runtime_plugins: #{Default}::default(),
behavior_major_version: #{Default}::default(),
}
}
""",
@ -367,11 +433,18 @@ class ServiceConfigGenerator(
customizations.forEach {
it.section(ServiceConfig.BuilderImpl)(this)
}
behaviorMv()(this)
val visibility = if (enableUserConfigurableRuntimePlugins) { "pub" } else { "pub(crate)" }
val visibility = if (enableUserConfigurableRuntimePlugins) {
"pub"
} else {
"pub(crate)"
}
docs("Adds a runtime plugin to the config.")
if (!enableUserConfigurableRuntimePlugins) { Attribute.AllowUnused.render(this) }
if (!enableUserConfigurableRuntimePlugins) {
Attribute.AllowUnused.render(this)
}
rustTemplate(
"""
$visibility fn runtime_plugin(mut self, plugin: impl #{RuntimePlugin} + 'static) -> Self {
@ -382,7 +455,9 @@ class ServiceConfigGenerator(
*codegenScope,
)
docs("Adds a runtime plugin to the config.")
if (!enableUserConfigurableRuntimePlugins) { Attribute.AllowUnused.render(this) }
if (!enableUserConfigurableRuntimePlugins) {
Attribute.AllowUnused.render(this)
}
rustTemplate(
"""
$visibility fn push_runtime_plugin(&mut self, plugin: #{SharedRuntimePlugin}) -> &mut Self {
@ -433,6 +508,7 @@ class ServiceConfigGenerator(
cloneable: layer,
runtime_components: self.runtime_components,
runtime_plugins: self.runtime_plugins,
behavior_major_version: self.behavior_major_version,
""",
*codegenScope,
)

View File

@ -18,7 +18,7 @@ import java.nio.file.Path
fun clientIntegrationTest(
model: Model,
params: IntegrationTestParams = IntegrationTestParams(),
params: IntegrationTestParams = IntegrationTestParams(cargoCommand = "cargo test --features behavior-version-latest"),
additionalDecorators: List<ClientCodegenDecorator> = listOf(),
test: (ClientCodegenContext, RustCrate) -> Unit = { _, _ -> },
): Path {

View File

@ -1,100 +0,0 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.client.testutil
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.generators.config.ConfigCustomization
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfigGenerator
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.configParamNewtype
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
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.RuntimeType
import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace
import software.amazon.smithy.rust.codegen.core.testutil.TestWriterDelegator
import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest
import software.amazon.smithy.rust.codegen.core.testutil.unitTest
import software.amazon.smithy.rust.codegen.core.util.toPascalCase
/**
* Test helper to produce a valid config customization to test that a [ConfigCustomization] can be used in conjunction
* with other [ConfigCustomization]s.
*/
fun stubConfigCustomization(name: String, codegenContext: ClientCodegenContext): ConfigCustomization {
return object : ConfigCustomization() {
override fun section(section: ServiceConfig): Writable = writable {
when (section) {
ServiceConfig.ConfigImpl -> {
rustTemplate(
"""
##[allow(missing_docs)]
pub fn $name(&self) -> u64 {
self.config.load::<#{T}>().map(|u| u.0).unwrap()
}
""",
"T" to configParamNewtype(
"_$name".toPascalCase(), RuntimeType.U64.toSymbol(),
codegenContext.runtimeConfig,
),
)
}
ServiceConfig.BuilderImpl -> {
rustTemplate(
"""
/// docs!
pub fn $name(mut self, $name: u64) -> Self {
self.config.store_put(#{T}($name));
self
}
""",
"T" to configParamNewtype(
"_$name".toPascalCase(), RuntimeType.U64.toSymbol(),
codegenContext.runtimeConfig,
),
)
}
else -> emptySection
}
}
}
}
/** Basic validation of [ConfigCustomization]s
*
* This test is not comprehensive, but it ensures that your customization generates Rust code that compiles and correctly
* composes with other customizations.
* */
@Suppress("NAME_SHADOWING")
fun validateConfigCustomizations(
codegenContext: ClientCodegenContext,
customization: ConfigCustomization,
project: TestWriterDelegator? = null,
): TestWriterDelegator {
val project = project ?: TestWorkspace.testProject()
stubConfigProject(codegenContext, customization, project)
project.compileAndTest()
return project
}
fun stubConfigProject(codegenContext: ClientCodegenContext, customization: ConfigCustomization, project: TestWriterDelegator): TestWriterDelegator {
val customizations = listOf(stubConfigCustomization("a", codegenContext)) + customization + stubConfigCustomization("b", codegenContext)
val generator = ServiceConfigGenerator(codegenContext, customizations = customizations.toList())
project.withModule(ClientRustModule.config) {
generator.render(this)
unitTest(
"config_send_sync",
"""
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Config>();
""",
)
}
project.lib { rust("pub use config::Config;") }
return project
}

View File

@ -99,3 +99,5 @@ fun TestWriterDelegator.clientRustSettings() =
moduleName = "test_${baseDir.toFile().nameWithoutExtension}",
codegenConfig = codegenConfig as ClientCodegenConfig,
)
fun TestWriterDelegator.clientCodegenContext(model: Model) = testClientCodegenContext(model, settings = clientRustSettings())

View File

@ -11,29 +11,15 @@ import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency
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.asSmithyModel
import software.amazon.smithy.rust.codegen.core.testutil.BasicTestModels
import software.amazon.smithy.rust.codegen.core.testutil.testModule
import software.amazon.smithy.rust.codegen.core.testutil.tokioTest
class MetadataCustomizationTest {
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 `extract metadata via customizable operation`() {
clientIntegrationTest(model) { clientCodegenContext, rustCrate ->
clientIntegrationTest(BasicTestModels.AwsJson10TestModel) { clientCodegenContext, rustCrate ->
val runtimeConfig = clientCodegenContext.runtimeConfig
val codegenScope = arrayOf(
*preludeScope,

View File

@ -6,49 +6,25 @@
package software.amazon.smithy.rust.codegen.client.smithy.customizations
import org.junit.jupiter.api.Test
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenConfig
import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule
import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginGenerator
import software.amazon.smithy.rust.codegen.client.testutil.clientRustSettings
import software.amazon.smithy.rust.codegen.client.testutil.stubConfigProject
import software.amazon.smithy.rust.codegen.client.testutil.testClientCodegenContext
import software.amazon.smithy.rust.codegen.core.smithy.transformers.OperationNormalizer
import software.amazon.smithy.rust.codegen.core.smithy.transformers.RecursiveShapeBoxer
import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest
import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.core.testutil.BasicTestModels
import software.amazon.smithy.rust.codegen.core.testutil.unitTest
internal class ResiliencyConfigCustomizationTest {
private val baseModel = """
namespace test
use aws.protocols#awsQuery
structure SomeOutput {
@xmlAttribute
someAttribute: Long,
someVal: String
}
operation SomeOperation {
output: SomeOutput
}
""".asSmithyModel()
@Test
fun `generates a valid config`() {
val model = RecursiveShapeBoxer().transform(OperationNormalizer.transform(baseModel))
val project = TestWorkspace.testProject(model, ClientCodegenConfig())
val codegenContext = testClientCodegenContext(model, settings = project.clientRustSettings())
stubConfigProject(codegenContext, ResiliencyConfigCustomization(codegenContext), project)
project.withModule(ClientRustModule.config) {
ServiceRuntimePluginGenerator(codegenContext).render(
this,
emptyList(),
)
clientIntegrationTest(BasicTestModels.AwsJson10TestModel) { _, crate ->
crate.unitTest("resiliency_fields") {
rustTemplate(
"""
let mut conf = crate::Config::builder();
conf.set_sleep_impl(None);
conf.set_retry_config(None);
""",
)
}
}
ResiliencyReExportCustomization(codegenContext).extras(project)
project.compileAndTest()
}
}

View File

@ -6,11 +6,8 @@
package software.amazon.smithy.rust.codegen.client.smithy.endpoint
import org.junit.jupiter.api.Test
import software.amazon.smithy.rust.codegen.client.testutil.testClientCodegenContext
import software.amazon.smithy.rust.codegen.client.testutil.validateConfigCustomizations
import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest
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.TestWorkspace
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
import software.amazon.smithy.rust.codegen.core.testutil.unitTest
@ -18,6 +15,7 @@ class ClientContextConfigCustomizationTest {
val model = """
namespace test
use smithy.rules#clientContextParams
use aws.protocols#awsJson1_0
@clientContextParams(aStringParam: {
documentation: "string docs",
@ -27,58 +25,54 @@ class ClientContextConfigCustomizationTest {
documentation: "bool docs",
type: "boolean"
})
@awsJson1_0
service TestService { operations: [] }
""".asSmithyModel()
@Test
fun `client params generate a valid customization`() {
val project = TestWorkspace.testProject()
val context = testClientCodegenContext(model)
project.unitTest {
rustTemplate(
"""
use #{RuntimePlugin};
let conf = crate::Config::builder().a_string_param("hello!").a_bool_param(true).build();
assert_eq!(
conf.config
.load::<crate::config::AStringParam>()
.map(|u| u.0.clone())
.unwrap(),
"hello!"
);
assert_eq!(
conf.config
.load::<crate::config::ABoolParam>()
.map(|u| u.0),
Some(true)
);
""",
"RuntimePlugin" to RuntimeType.runtimePlugin(context.runtimeConfig),
)
clientIntegrationTest(model) { _, crate ->
crate.unitTest {
rustTemplate(
"""
let conf = crate::Config::builder().a_string_param("hello!").a_bool_param(true).build();
assert_eq!(
conf.config
.load::<crate::config::AStringParam>()
.map(|u| u.0.clone())
.unwrap(),
"hello!"
);
assert_eq!(
conf.config
.load::<crate::config::ABoolParam>()
.map(|u| u.0),
Some(true)
);
""",
)
}
crate.unitTest("unset_fields") {
rustTemplate(
"""
let conf = crate::Config::builder().a_string_param("hello!").build();
assert_eq!(
conf.config
.load::<crate::config::AStringParam>()
.map(|u| u.0.clone())
.unwrap(),
"hello!"
);
assert_eq!(
conf.config
.load::<crate::config::ABoolParam>()
.map(|u| u.0),
None,
);
""",
)
}
}
// unset fields
project.unitTest {
rustTemplate(
"""
use #{RuntimePlugin};
let conf = crate::Config::builder().a_string_param("hello!").build();
assert_eq!(
conf.config
.load::<crate::config::AStringParam>()
.map(|u| u.0.clone())
.unwrap(),
"hello!"
);
assert_eq!(
conf.config
.load::<crate::config::ABoolParam>()
.map(|u| u.0),
None,
);
""",
"RuntimePlugin" to RuntimeType.runtimePlugin(context.runtimeConfig),
)
}
validateConfigCustomizations(context, ClientContextConfigCustomization(context), project)
}
}

View File

@ -128,7 +128,7 @@ class EndpointsDecoratorTest {
val testDir = clientIntegrationTest(
model,
// Just run integration tests.
IntegrationTestParams(command = { "cargo test --test *".runWithWarnings(it) }),
IntegrationTestParams(command = { "cargo test --all-features --test *".runWithWarnings(it) }),
) { clientCodegenContext, rustCrate ->
rustCrate.integrationTest("endpoint_params_test") {
val moduleName = clientCodegenContext.moduleUseName()

View File

@ -106,7 +106,7 @@ class ErrorCorrectionTest {
assert_eq!(shape.not_required(), None);
// set defaults for everything else
assert_eq!(shape.blob().as_ref(), &[]);
assert_eq!(shape.blob().as_ref(), b"");
assert!(shape.list_value().is_empty());
assert!(shape.map_value().is_empty());

View File

@ -1,23 +0,0 @@
/*
* 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.config
import org.junit.jupiter.api.Test
import software.amazon.smithy.rust.codegen.client.testutil.testClientCodegenContext
import software.amazon.smithy.rust.codegen.client.testutil.validateConfigCustomizations
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
class IdempotencyTokenProviderCustomizationTest {
@Test
fun `generates a valid config`() {
val model = "namespace test".asSmithyModel()
val codegenContext = testClientCodegenContext(model)
validateConfigCustomizations(
codegenContext,
IdempotencyTokenProviderCustomization(codegenContext),
)
}
}

View File

@ -10,16 +10,15 @@ import org.junit.jupiter.api.Test
import software.amazon.smithy.model.shapes.ServiceShape
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.testutil.testClientCodegenContext
import software.amazon.smithy.rust.codegen.client.testutil.withEnableUserConfigurableRuntimePlugins
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
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.NamedCustomization
import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace
import software.amazon.smithy.rust.codegen.core.testutil.BasicTestModels
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest
import software.amazon.smithy.rust.codegen.core.testutil.unitTest
import software.amazon.smithy.rust.codegen.core.util.lookup
import software.amazon.smithy.rust.codegen.core.util.toPascalCase
@ -125,45 +124,50 @@ internal class ServiceConfigGeneratorTest {
}
}
val model = "namespace empty".asSmithyModel()
val codegenContext = testClientCodegenContext(model)
.withEnableUserConfigurableRuntimePlugins(true)
val sut = ServiceConfigGenerator(codegenContext, listOf(ServiceCustomizer(codegenContext)))
val symbolProvider = codegenContext.symbolProvider
val project = TestWorkspace.testProject(symbolProvider)
project.withModule(ClientRustModule.config) {
sut.render(this)
unitTest(
"set_config_fields",
"""
let builder = Config::builder().config_field(99);
let config = builder.build();
assert_eq!(config.config_field(), 99);
""",
)
unitTest(
"set_runtime_plugin",
"""
use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin;
use aws_smithy_types::config_bag::FrozenLayer;
#[derive(Debug)]
struct TestRuntimePlugin;
impl RuntimePlugin for TestRuntimePlugin {
fn config(&self) -> Option<FrozenLayer> {
todo!("ExampleRuntimePlugin.config")
}
}
let config = Config::builder()
.runtime_plugin(TestRuntimePlugin)
.build();
assert_eq!(config.runtime_plugins.len(), 1);
""",
)
val serviceDecorator = object : ClientCodegenDecorator {
override val name: String = "Add service plugin"
override val order: Byte = 0
override fun configCustomizations(
codegenContext: ClientCodegenContext,
baseCustomizations: List<ConfigCustomization>,
): List<ConfigCustomization> {
return baseCustomizations + ServiceCustomizer(codegenContext)
}
}
clientIntegrationTest(BasicTestModels.AwsJson10TestModel, additionalDecorators = listOf(serviceDecorator)) { ctx, rustCrate ->
rustCrate.withModule(ClientRustModule.config) {
unitTest(
"set_config_fields",
"""
let builder = Config::builder().config_field(99);
let config = builder.build();
assert_eq!(config.config_field(), 99);
""",
)
unitTest(
"set_runtime_plugin",
"""
use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin;
use aws_smithy_types::config_bag::FrozenLayer;
#[derive(Debug)]
struct TestRuntimePlugin;
impl RuntimePlugin for TestRuntimePlugin {
fn config(&self) -> Option<FrozenLayer> {
todo!("ExampleRuntimePlugin.config")
}
}
let config = Config::builder()
.runtime_plugin(TestRuntimePlugin)
.build();
assert_eq!(config.runtime_plugins.len(), 1);
""",
)
}
}
project.compileAndTest()
}
}

View File

@ -449,8 +449,23 @@ fun RustWriter.implBlock(symbol: Symbol, block: Writable) {
/** Write a `#[cfg(feature = "...")]` block for the given feature */
fun RustWriter.featureGateBlock(feature: String, block: Writable) {
rustBlock("##[cfg(feature = ${feature.dq()})]") {
block()
featureGatedBlock(feature, block)(this)
}
/** Write a `#[cfg(feature = "...")]` block for the given feature */
fun featureGatedBlock(feature: String, block: Writable): Writable {
return writable {
rustBlock("##[cfg(feature = ${feature.dq()})]") {
block()
}
}
}
fun featureGatedBlock(feature: Feature, block: Writable): Writable {
return writable {
rustBlock("##[cfg(feature = ${feature.name.dq()})]") {
block()
}
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.core.testutil
object BasicTestModels {
val AwsJson10TestModel = """
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()
}

View File

@ -21,6 +21,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWordConfig
import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWordSymbolProvider
import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords
import software.amazon.smithy.rust.codegen.core.rustlang.Visibility
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
import software.amazon.smithy.rust.codegen.core.rustlang.implBlock
import software.amazon.smithy.rust.codegen.core.smithy.BaseSymbolMetadataProvider
import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext
@ -138,7 +139,11 @@ fun testRustSettings(
)
private const val SmithyVersion = "1.0"
fun String.asSmithyModel(sourceLocation: String? = null, smithyVersion: String = SmithyVersion, disableValidation: Boolean = false): Model {
fun String.asSmithyModel(
sourceLocation: String? = null,
smithyVersion: String = SmithyVersion,
disableValidation: Boolean = false,
): Model {
val processed = letIf(!this.trimStart().startsWith("\$version")) { "\$version: ${smithyVersion.dq()}\n$it" }
val assembler = Model.assembler().discoverModels().addUnparsedModel(sourceLocation ?: "test.smithy", processed)
if (disableValidation) {
@ -208,3 +213,10 @@ fun StructureShape.renderWithModelBuilder(
BuilderGenerator(model, symbolProvider, struct, emptyList()).render(this)
}
}
fun RustCrate.unitTest(name: String? = null, test: Writable) {
lib {
val testName = name ?: safeName("test")
unitTest(testName, block = test)
}
}

View File

@ -66,6 +66,7 @@
- [RFC-0037: The HTTP Wrapper](./rfcs/rfc0037_http_wrapper.md)
- [RFC-0038: User-configurable retry classification](./rfcs/rfc0038_retry_classifier_customization.md)
- [RFC-0039: Forward Compatible Errors](./rfcs/rfc0039_forward_compatible_errors.md)
- [RFC-0040: Behavior Major Versions](./rfcs/rfc0040_behavior_major_versions.md)
- [Contributing](./contributing/overview.md)
- [Writing and debugging a low-level feature that relies on HTTP](./contributing/writing_and_debugging_a_low-level_feature_that_relies_on_HTTP.md)

View File

@ -0,0 +1,86 @@
<!-- Give your RFC a descriptive name saying what it would accomplish or what feature it defines -->
RFC: Behavior Major Versions
=============
<!-- RFCs start with the "RFC" status and are then either "Implemented" or "Rejected". -->
> Status: RFC
>
> Applies to: client
<!-- A great RFC will include a list of changes at the bottom so that the implementor can be sure they haven't missed anything -->
For a summarized list of proposed changes, see the [Changes Checklist](#changes-checklist) section.
<!-- Insert a short paragraph explaining, at a high level, what this RFC is for -->
This RFC describes "Behavior Major Versions," a mechanism to allow SDKs to ship breaking behavioral changes like a new retry strategy, while allowing customers who rely on extremely consistent behavior to evolve at their own pace.
By adding behavior major versions (BMV) to the Rust SDK, we will make it possible to ship new secure/recommended defaults to new customers without impacting legacy customers.
The fundamental issue stems around our inability to communicate and decouple releases of service updates and behavior within a single major version.
Both legacy and new SDKs have the need to alter their SDKs default. Historically, this caused new customers on legacy SDKs to be subject to legacy defaults, even when a better alternative existed.
For new SDKs, a GA cutline presents difficult choices around timeline and features that cant be added later without altering behavior.
Both of these use cases are addressed by Behavior Major Versions.
<!-- Explain how users will use this new feature and, if necessary, how this compares to the current user experience -->
The user experience if this RFC is implemented
----------------------------------------------
In the current version of the SDK, users can construct clients without indicating any sort of behavior major version.
Once this RFC is implemented, there will be two ways to set a behavior major version:
1. In code via `aws_config::from_env_with_version(BehaviorMajorVersion::latest())` and `<service>::Config::builder().behavior_major_version(...)`. This will also work for `config_override`.
2. By enabling `behavior-version-latest` in either `aws-config` (which brings back `from_env`) OR a specific generated SDK crate
```toml
# Cargo.toml
[dependencies]
aws-config = { version = "1", features = ["behavior-version-latest"] }
# OR
aws-sdk-s3 = { version = "1", features = ["behavior-version-latest"] }
```
If no `BehaviorMajorVersion` is set, the client will panic during construction.
`BehaviorMajorVersion` is an opaque struct with initializers like `::latest()`, `::v2023_11_09()`. Downstream code can check the version by calling methods like `::supports_v1()`
When new BMV are added, the previous version constructor will be marked as `deprecated`. This serves as a mechanism to alert customers that a new BMV exists to allow them to upgrade.
How to actually implement this RFC
----------------------------------
In order to implement this feature, we need to create a `BehaviorMajorVersion` struct, add config options to `SdkConfig` and `aws-config`, and wire it throughout the stack.
```rust
/// Behavior major-version of the client
///
/// Over time, new best-practice behaviors are introduced. However, these behaviors might not be backwards
/// compatible. For example, a change which introduces new default timeouts or a new retry-mode for
/// all operations might be the ideal behavior but could break existing applications.
#[derive(Debug, Clone)]
pub struct BehaviorMajorVersion {
// currently there is only 1 MV so we don't actually need anything in here.
}
```
To help customers migrate, we are including `from_env` hooks that set `behavior-version-latest` that are _deprecated_. This allows customers to see that they are missing the required cargo feature and add it to remove the deprecation warning.
Internally, `BehaviorMajorVersion` will become an additional field on `<client>::Config`. It is _not_ ever stored in the `ConfigBag` or in `RuntimePlugins`.
When constructing the set of "default runtime plugins," the default runtime plugin parameters will be passed the `BehaviorMajorVersion`. This will select the correct runtime plugin. Logging will clearly indicate which plugin was selected.
Design Alternatives Considered
------------------------------
An original design was also considered that made BMV optional and relied on documentation to steer customers in the right direction. This was
deemed too weak of a mechanism to ensure that customers aren't broken by unexpected changes.
Changes checklist
-----------------
- [x] Create `BehaviorMajorVersion` and the BMV runtime plugin
- [x] Add BMV as a required runtime component
- [x] Wire up setters throughout the stack
- [x] Add tests of BMV (set via aws-config, cargo features & code params)
- [x] ~Remove `aws_config::from_env` deprecation stand-ins~ We decided to persist these deprecations
- [x] Update generated usage examples

View File

@ -13,7 +13,7 @@ publish = false
# eliminating the need to directly depend on the crates that provide them. In rare instances,
# you may still need to include one of these crates as a dependency. Examples that require this
# are specifically noted in comments above the corresponding dependency in this file.
pokemon-service-client = { path = "../pokemon-service-client/" }
pokemon-service-client = { path = "../pokemon-service-client/", features = ["behavior-version-latest"] }
# Required for getting the operation name from the `Metadata`.
aws-smithy-http = { path = "../../rust-runtime/aws-smithy-http/" }

View File

@ -20,4 +20,4 @@ hyper-rustls = { version = "0.24", features = ["http2"] }
aws-smithy-runtime = { path = "../../../rust-runtime/aws-smithy-runtime/", features = ["client", "connector-hyper-0-14-x"] }
aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http/" }
aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types/" }
pokemon-service-client = { path = "../pokemon-service-client/" }
pokemon-service-client = { path = "../pokemon-service-client/", features = ["behavior-version-latest"] }

View File

@ -116,4 +116,5 @@ pub mod runtime_components;
pub mod runtime_plugin;
pub mod behavior_version;
pub mod ser_de;

View File

@ -0,0 +1,42 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
//! Behavior Major version of the client
/// Behavior major-version of the client
///
/// Over time, new best-practice behaviors are introduced. However, these behaviors might not be backwards
/// compatible. For example, a change which introduces new default timeouts or a new retry-mode for
/// all operations might be the ideal behavior but could break existing applications.
#[derive(Debug, Clone)]
pub struct BehaviorMajorVersion {}
impl BehaviorMajorVersion {
/// This method will always return the latest major version.
///
/// This is the recommend choice for customers who aren't reliant on extremely specific behavior
/// characteristics. For example, if you are writing a CLI app, the latest behavior major version
/// is probably the best setting for you.
///
/// If, however, you're writing a service that is very latency sensitive, or that has written
/// code to tune Rust SDK behaviors, consider pinning to a specific major version.
///
/// The latest version is currently [`BehaviorMajorVersion::v2023_11_09`]
pub fn latest() -> Self {
Self {}
}
/// This method returns the behavior configuration for November 9th, 2023
///
/// When a new behavior major version is released, this method will be deprecated.
pub fn v2023_11_09() -> Self {
Self {}
}
/// Returns whether the current version is `v2023_11_09`
pub fn supports_v2023_11_09(&self) -> bool {
true
}
}

View File

@ -474,7 +474,7 @@ impl RuntimeComponentsBuilder {
auth_scheme_option_resolver: Option<impl ResolveAuthSchemeOptions + 'static>,
) -> &mut Self {
self.auth_scheme_option_resolver =
auth_scheme_option_resolver.map(|r| Tracked::new(self.builder_name, r.into_shared()));
self.tracked(auth_scheme_option_resolver.map(IntoShared::into_shared));
self
}
@ -494,7 +494,7 @@ impl RuntimeComponentsBuilder {
/// Sets the HTTP client.
pub fn set_http_client(&mut self, connector: Option<impl HttpClient + 'static>) -> &mut Self {
self.http_client = connector.map(|c| Tracked::new(self.builder_name, c.into_shared()));
self.http_client = self.tracked(connector.map(IntoShared::into_shared));
self
}
@ -717,13 +717,13 @@ impl RuntimeComponentsBuilder {
/// Sets the async sleep implementation.
pub fn set_sleep_impl(&mut self, sleep_impl: Option<SharedAsyncSleep>) -> &mut Self {
self.sleep_impl = sleep_impl.map(|s| Tracked::new(self.builder_name, s));
self.sleep_impl = self.tracked(sleep_impl);
self
}
/// Sets the async sleep implementation.
pub fn with_sleep_impl(mut self, sleep_impl: Option<impl AsyncSleep + 'static>) -> Self {
self.sleep_impl = sleep_impl.map(|s| Tracked::new(self.builder_name, s.into_shared()));
self.set_sleep_impl(sleep_impl.map(IntoShared::into_shared));
self
}
@ -734,13 +734,13 @@ impl RuntimeComponentsBuilder {
/// Sets the time source.
pub fn set_time_source(&mut self, time_source: Option<SharedTimeSource>) -> &mut Self {
self.time_source = time_source.map(|s| Tracked::new(self.builder_name, s));
self.time_source = self.tracked(time_source);
self
}
/// Sets the time source.
pub fn with_time_source(mut self, time_source: Option<impl TimeSource + 'static>) -> Self {
self.time_source = time_source.map(|s| Tracked::new(self.builder_name, s.into_shared()));
self.set_time_source(time_source.map(IntoShared::into_shared));
self
}
@ -805,6 +805,11 @@ impl RuntimeComponentsBuilder {
validate!(self.retry_strategy);
Ok(())
}
/// Wraps `v` in tracking associated with this builder
fn tracked<T>(&self, v: Option<T>) -> Option<Tracked<T>> {
v.map(|v| Tracked::new(self.builder_name, v))
}
}
#[derive(Clone, Debug)]

View File

@ -15,6 +15,7 @@ use crate::client::retries::RetryPartition;
use aws_smithy_async::rt::sleep::default_async_sleep;
use aws_smithy_async::time::SystemTimeSource;
use aws_smithy_runtime_api::box_error::BoxError;
use aws_smithy_runtime_api::client::behavior_version::BehaviorMajorVersion;
use aws_smithy_runtime_api::client::http::SharedHttpClient;
use aws_smithy_runtime_api::client::runtime_components::{
RuntimeComponentsBuilder, SharedConfigValidator,
@ -170,6 +171,7 @@ pub fn default_identity_cache_plugin() -> Option<SharedRuntimePlugin> {
#[derive(Debug, Default)]
pub struct DefaultPluginParams {
retry_partition_name: Option<Cow<'static, str>>,
behavior_major_version: Option<BehaviorMajorVersion>,
}
impl DefaultPluginParams {
@ -183,6 +185,12 @@ impl DefaultPluginParams {
self.retry_partition_name = Some(name.into());
self
}
/// Sets the behavior major version.
pub fn with_behavior_major_version(mut self, version: BehaviorMajorVersion) -> Self {
self.behavior_major_version = Some(version);
self
}
}
/// All default plugins.

View File

@ -27,12 +27,9 @@ pub struct CaptureRequestHandler(Arc<Mutex<Inner>>);
impl HttpConnector for CaptureRequestHandler {
fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
let mut inner = self.0.lock().unwrap();
inner
.sender
.take()
.expect("already sent")
.send(request)
.expect("channel not ready");
if let Err(_e) = inner.sender.take().expect("already sent").send(request) {
tracing::trace!("The receiver was already dropped");
}
HttpConnectorFuture::ready(Ok(inner
.response
.take()

View File

@ -76,6 +76,7 @@ struct LambdaMain {
}
impl LambdaMain {
#[allow(deprecated)]
async fn new() -> Self {
Self {
sdk_config: aws_config::load_from_env().await,