Skip to content

Commit 41c3578

Browse files
authored
Merge branch 'main' into feature-ids-implementation
2 parents 903f043 + 80e9ab4 commit 41c3578

File tree

13 files changed

+358
-31
lines changed

13 files changed

+358
-31
lines changed

.changelog/1762538321.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
applies_to: ["client"]
3+
authors:
4+
- greenwoodcm
5+
references:
6+
- smithy-rs#4388
7+
breaking: false
8+
new_feature: false
9+
bug_fix: false
10+
---
11+
Add `then_compute_response` to Smithy mock

aws/codegen-aws-sdk/src/test/kotlin/software/amazon/smithy/rustsdk/UserAgentDecoratorTest.kt

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,82 @@ class UserAgentDecoratorTest {
145145
}
146146
}
147147

148+
@Test
149+
fun `it avoids emitting repeated business metrics on retry`() {
150+
awsSdkIntegrationTest(model) { context, rustCrate ->
151+
rustCrate.integrationTest("business_metrics") {
152+
tokioTest("metrics_should_not_be_repeated") {
153+
val rc = context.runtimeConfig
154+
val moduleName = context.moduleUseName()
155+
rustTemplate(
156+
"""
157+
use $moduleName::config::{AppName, Credentials, Region, SharedCredentialsProvider};
158+
use $moduleName::{Config, Client};
159+
160+
let http_client = #{StaticReplayClient}::new(vec![
161+
#{ReplayEvent}::new(
162+
#{http}::Request::builder()
163+
.uri("http://localhost:1234/")
164+
.body(#{SdkBody}::empty())
165+
.unwrap(),
166+
#{http}::Response::builder()
167+
.status(500)
168+
.body(#{SdkBody}::empty())
169+
.unwrap(),
170+
),
171+
#{ReplayEvent}::new(
172+
#{http}::Request::builder()
173+
.uri("http://localhost:1234/")
174+
.body(#{SdkBody}::empty())
175+
.unwrap(),
176+
#{http}::Response::builder()
177+
.status(200)
178+
.body(#{SdkBody}::empty())
179+
.unwrap(),
180+
),
181+
]);
182+
183+
let mut creds = Credentials::for_tests();
184+
creds.get_property_mut_or_default::<Vec<#{AwsCredentialFeature}>>()
185+
.push(#{AwsCredentialFeature}::CredentialsEnvVars);
186+
187+
let config = Config::builder()
188+
.credentials_provider(SharedCredentialsProvider::new(creds))
189+
.retry_config(#{RetryConfig}::standard().with_max_attempts(2))
190+
.region(Region::new("us-east-1"))
191+
.http_client(http_client.clone())
192+
.app_name(AppName::new("test-app-name").expect("valid app name"))
193+
.build();
194+
let client = Client::from_conf(config);
195+
let _ = client.some_operation().send().await;
196+
197+
let req = http_client.actual_requests().last().unwrap();
198+
let aws_ua_header = req.headers().get("x-amz-user-agent").unwrap();
199+
let metrics_section = aws_ua_header
200+
.split(" m/")
201+
.nth(1)
202+
.unwrap()
203+
.split_ascii_whitespace()
204+
.nth(0)
205+
.unwrap();
206+
assert_eq!(1, metrics_section.matches("g").count());
207+
""",
208+
"http" to RuntimeType.Http,
209+
"AwsCredentialFeature" to
210+
AwsRuntimeType.awsCredentialTypes(rc)
211+
.resolve("credential_feature::AwsCredentialFeature"),
212+
"RetryConfig" to RuntimeType.smithyTypes(rc).resolve("retry::RetryConfig"),
213+
"ReplayEvent" to RuntimeType.smithyHttpClientTestUtil(rc).resolve("test_util::ReplayEvent"),
214+
"SdkBody" to RuntimeType.sdkBody(rc),
215+
"StaticReplayClient" to
216+
RuntimeType.smithyHttpClientTestUtil(rc)
217+
.resolve("test_util::StaticReplayClient"),
218+
)
219+
}
220+
}
221+
}
222+
}
223+
148224
@Test
149225
fun `it emits business metric for RPC v2 CBOR in user agent`() {
150226
val model =

aws/rust-runtime/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aws/rust-runtime/aws-runtime/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aws-runtime"
3-
version = "1.5.15"
3+
version = "1.5.16"
44
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
55
description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly."
66
edition = "2021"

aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs

Lines changed: 114 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,36 @@ use crate::sdk_feature::AwsSdkFeature;
2525
use crate::user_agent::metrics::ProvideBusinessMetric;
2626
use crate::user_agent::{AdditionalMetadata, ApiMetadata, AwsUserAgent, InvalidMetadataValue};
2727

28+
macro_rules! add_metrics_unique {
29+
($features:expr, $ua:expr, $added:expr) => {
30+
for feature in $features {
31+
if let Some(m) = feature.provide_business_metric() {
32+
if !$added.contains(&m) {
33+
$added.insert(m.clone());
34+
$ua.add_business_metric(m);
35+
}
36+
}
37+
}
38+
};
39+
}
40+
41+
macro_rules! add_metrics_unique_reverse {
42+
($features:expr, $ua:expr, $added:expr) => {
43+
let mut unique_metrics = Vec::new();
44+
for feature in $features {
45+
if let Some(m) = feature.provide_business_metric() {
46+
if !$added.contains(&m) {
47+
$added.insert(m.clone());
48+
unique_metrics.push(m);
49+
}
50+
}
51+
}
52+
for m in unique_metrics.into_iter().rev() {
53+
$ua.add_business_metric(m);
54+
}
55+
};
56+
}
57+
2858
#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this
2959
const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent");
3060

@@ -133,30 +163,17 @@ impl Intercept for UserAgentInterceptor {
133163
.expect("`AwsUserAgent should have been created in `read_before_execution`")
134164
.clone();
135165

136-
// Load features from ConfigBag. Note: cfg.load() automatically captures features
137-
// from all layers in the ConfigBag, including the interceptor_state layer where
138-
// interceptors store their features. There is no need to separately call
139-
// cfg.interceptor_state().load() as that would be redundant.
140-
let smithy_sdk_features = cfg.load::<SmithySdkFeature>();
141-
for smithy_sdk_feature in smithy_sdk_features {
142-
smithy_sdk_feature
143-
.provide_business_metric()
144-
.map(|m| ua.add_business_metric(m));
145-
}
166+
let mut added_metrics = std::collections::HashSet::new();
146167

147-
let aws_sdk_features = cfg.load::<AwsSdkFeature>();
148-
for aws_sdk_feature in aws_sdk_features {
149-
aws_sdk_feature
150-
.provide_business_metric()
151-
.map(|m| ua.add_business_metric(m));
152-
}
153-
154-
let aws_credential_features = cfg.load::<AwsCredentialFeature>();
155-
for aws_credential_feature in aws_credential_features {
156-
aws_credential_feature
157-
.provide_business_metric()
158-
.map(|m| ua.add_business_metric(m));
159-
}
168+
add_metrics_unique!(cfg.load::<SmithySdkFeature>(), &mut ua, &mut added_metrics);
169+
add_metrics_unique!(cfg.load::<AwsSdkFeature>(), &mut ua, &mut added_metrics);
170+
// The order we emit credential features matters.
171+
// Reverse to preserve emission order since StoreAppend pops backwards.
172+
add_metrics_unique_reverse!(
173+
cfg.load::<AwsCredentialFeature>(),
174+
&mut ua,
175+
&mut added_metrics
176+
);
160177

161178
let maybe_connector_metadata = runtime_components
162179
.http_client()
@@ -263,6 +280,80 @@ mod tests {
263280
);
264281
}
265282

283+
#[test]
284+
fn test_modify_before_signing_no_duplicate_metrics() {
285+
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
286+
let mut context = context();
287+
288+
let api_metadata = ApiMetadata::new("test-service", "1.0");
289+
let mut layer = Layer::new("test");
290+
layer.store_put(api_metadata);
291+
// Duplicate features
292+
layer.store_append(SmithySdkFeature::Waiter);
293+
layer.store_append(SmithySdkFeature::Waiter);
294+
layer.store_append(AwsSdkFeature::S3Transfer);
295+
layer.store_append(AwsSdkFeature::S3Transfer);
296+
layer.store_append(AwsCredentialFeature::CredentialsCode);
297+
layer.store_append(AwsCredentialFeature::CredentialsCode);
298+
let mut config = ConfigBag::of_layers(vec![layer]);
299+
300+
let interceptor = UserAgentInterceptor::new();
301+
let ctx = Into::into(&context);
302+
interceptor
303+
.read_after_serialization(&ctx, &rc, &mut config)
304+
.unwrap();
305+
let mut ctx = Into::into(&mut context);
306+
interceptor
307+
.modify_before_signing(&mut ctx, &rc, &mut config)
308+
.unwrap();
309+
310+
let aws_ua_header = expect_header(&context, "x-amz-user-agent");
311+
let metrics_section = aws_ua_header.split(" m/").nth(1).unwrap();
312+
let waiter_count = metrics_section.matches("B").count();
313+
let s3_transfer_count = metrics_section.matches("G").count();
314+
let credentials_code_count = metrics_section.matches("e").count();
315+
assert_eq!(
316+
1, waiter_count,
317+
"Waiter metric should appear only once, but found {waiter_count} occurrences in: {aws_ua_header}",
318+
);
319+
assert_eq!(1, s3_transfer_count, "S3Transfer metric should appear only once, but found {s3_transfer_count} occurrences in metrics section: {aws_ua_header}");
320+
assert_eq!(1, credentials_code_count, "CredentialsCode metric should appear only once, but found {credentials_code_count} occurrences in metrics section: {aws_ua_header}");
321+
}
322+
323+
#[test]
324+
fn test_metrics_order_preserved() {
325+
use aws_credential_types::credential_feature::AwsCredentialFeature;
326+
327+
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
328+
let mut context = context();
329+
330+
let api_metadata = ApiMetadata::new("test-service", "1.0");
331+
let mut layer = Layer::new("test");
332+
layer.store_put(api_metadata);
333+
layer.store_append(AwsCredentialFeature::CredentialsCode);
334+
layer.store_append(AwsCredentialFeature::CredentialsEnvVars);
335+
layer.store_append(AwsCredentialFeature::CredentialsProfile);
336+
let mut config = ConfigBag::of_layers(vec![layer]);
337+
338+
let interceptor = UserAgentInterceptor::new();
339+
let ctx = Into::into(&context);
340+
interceptor
341+
.read_after_serialization(&ctx, &rc, &mut config)
342+
.unwrap();
343+
let mut ctx = Into::into(&mut context);
344+
interceptor
345+
.modify_before_signing(&mut ctx, &rc, &mut config)
346+
.unwrap();
347+
348+
let aws_ua_header = expect_header(&context, "x-amz-user-agent");
349+
let metrics_section = aws_ua_header.split(" m/").nth(1).unwrap();
350+
351+
assert_eq!(
352+
metrics_section, "e,g,n",
353+
"AwsCredentialFeature metrics should preserve order"
354+
);
355+
}
356+
266357
#[test]
267358
fn test_app_name() {
268359
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null)
319319

320320
fun smithyHttp(runtimeConfig: RuntimeConfig) = CargoDependency.smithyHttp(runtimeConfig).toType()
321321

322+
fun smithyHttpClientTestUtil(runtimeConfig: RuntimeConfig) =
323+
CargoDependency.smithyHttpClientTestUtil(runtimeConfig).toType()
324+
322325
fun smithyJson(runtimeConfig: RuntimeConfig) = CargoDependency.smithyJson(runtimeConfig).toType()
323326

324327
fun smithyQuery(runtimeConfig: RuntimeConfig) = CargoDependency.smithyQuery(runtimeConfig).toType()

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/UserProvidedValidationExceptionDecorator.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ class UserProvidedValidationExceptionConversionGenerator(
252252
"ValidationException" to codegenContext.symbolProvider.toSymbol(validationExceptionStructure),
253253
"FieldCreation" to
254254
writable {
255-
if (maybeValidationFieldList?.maybeValidationFieldMessageMember != null) {
255+
if (maybeValidationFieldList != null) {
256256
rust("""let first_validation_exception_field = constraint_violation.as_validation_exception_field("".to_owned());""")
257257
}
258258
},
@@ -319,6 +319,7 @@ class UserProvidedValidationExceptionConversionGenerator(
319319
.get(stringTraitInfo) as LengthTrait
320320
rustTemplate(
321321
"""
322+
##[allow(unused_variables)]
322323
Self::Length(length) => #{ValidationExceptionField} {
323324
#{FieldAssignments}
324325
},
@@ -384,6 +385,7 @@ class UserProvidedValidationExceptionConversionGenerator(
384385
blobConstraintsInfo.forEach { blobLength ->
385386
rustTemplate(
386387
"""
388+
##[allow(unused_variables)]
387389
Self::Length(length) => #{ValidationExceptionField} {
388390
#{FieldAssignments}
389391
},
@@ -424,6 +426,7 @@ class UserProvidedValidationExceptionConversionGenerator(
424426
shape.getTrait<LengthTrait>()?.also {
425427
rustTemplate(
426428
"""
429+
##[allow(unused_variables)]
427430
Self::Length(length) => #{ValidationExceptionField} {
428431
#{FieldAssignments}
429432
},""",
@@ -557,6 +560,7 @@ class UserProvidedValidationExceptionConversionGenerator(
557560
is CollectionTraitInfo.Length -> {
558561
rustTemplate(
559562
"""
563+
##[allow(unused_variables)]
560564
Self::Length(length) => #{ValidationExceptionField} {
561565
#{FieldAssignments}
562566
},
@@ -640,7 +644,7 @@ class UserProvidedValidationExceptionConversionGenerator(
640644
val pathExpression = member.wrapValueIfOptional(rawPathExpression)
641645
val messageExpression = member.wrapValueIfOptional(rawMessageExpression)
642646
when {
643-
member.hasTrait(ValidationFieldNameTrait.ID) ->
647+
member.isValidationFieldName() ->
644648
"$memberName: $pathExpression"
645649

646650
member.hasTrait(ValidationFieldMessageTrait.ID) ->

codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/UserProvidedValidationExceptionDecoratorTest.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,4 +384,61 @@ internal class UserProvidedValidationExceptionDecoratorTest {
384384
fun `code compiles with custom validation exception using optionals`() {
385385
serverIntegrationTest(completeTestModelWithOptionals)
386386
}
387+
388+
private val completeTestModelWithImplicitNamesWithoutFieldMessage =
389+
"""
390+
namespace com.aws.example
391+
392+
use aws.protocols#restJson1
393+
use smithy.framework.rust#validationException
394+
use smithy.framework.rust#validationFieldList
395+
396+
@restJson1
397+
service CustomValidationExample {
398+
version: "1.0.0"
399+
operations: [
400+
TestOperation
401+
]
402+
errors: [
403+
MyCustomValidationException
404+
]
405+
}
406+
407+
@http(method: "POST", uri: "/test")
408+
operation TestOperation {
409+
input: TestInput
410+
}
411+
412+
structure TestInput {
413+
@required
414+
@length(min: 1, max: 10)
415+
name: String
416+
417+
@range(min: 1, max: 100)
418+
age: Integer
419+
}
420+
421+
@error("client")
422+
@httpError(400)
423+
@validationException
424+
structure MyCustomValidationException {
425+
message: String
426+
427+
@validationFieldList
428+
customFieldList: CustomValidationFieldList
429+
}
430+
431+
structure CustomValidationField {
432+
name: String,
433+
}
434+
435+
list CustomValidationFieldList {
436+
member: CustomValidationField
437+
}
438+
""".asSmithyModel(smithyVersion = "2.0")
439+
440+
@Test
441+
fun `code compiles with implicit message and field name and without field message`() {
442+
serverIntegrationTest(completeTestModelWithImplicitNamesWithoutFieldMessage)
443+
}
387444
}

0 commit comments

Comments
 (0)