diff --git a/.changelog/1729271936.md b/.changelog/1729271936.md new file mode 100644 index 0000000000..485a426db4 --- /dev/null +++ b/.changelog/1729271936.md @@ -0,0 +1,12 @@ +--- +applies_to: +- aws-sdk-rust +authors: +- ysaito1001 +references: +- smithy-rs#3883 +breaking: false +new_feature: false +bug_fix: false +--- +Client SDKs built with the `awsQueryCompatible` trait now include the `x-amzn-query-mode` header. This header signals the service that the clients are operating in compatible mode. diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryCompatibleTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryCompatibleTest.kt index dcb322b447..7de72b7d8f 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryCompatibleTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryCompatibleTest.kt @@ -6,25 +6,29 @@ package software.amazon.smithy.rust.codegen.client.smithy.protocols import org.junit.jupiter.api.Test -import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel -import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.core.testutil.testModule +import software.amazon.smithy.rust.codegen.core.testutil.tokioTest +import software.amazon.smithy.rust.codegen.core.util.letIf class AwsQueryCompatibleTest { - @Test - fun `aws-query-compatible json with aws query error should allow for retrieving error code and type from custom header`() { - val model = - """ + companion object { + const val prologue = """ namespace test use aws.protocols#awsJson1_0 use aws.protocols#awsQueryCompatible use aws.protocols#awsQueryError + """ - @awsQueryCompatible - @awsJson1_0 + const val awsjson10Trait = "@awsJson1_0" + const val awsQueryCompatibleTrait = "@awsQueryCompatible" + + fun testService(withAwsQueryError: Boolean = true) = + """ service TestService { version: "2023-02-20", operations: [SomeOperation] @@ -40,28 +44,37 @@ class AwsQueryCompatibleTest { a: String, b: Integer } - - @awsQueryError( - code: "InvalidThing", - httpResponseCode: 400, - ) - @error("client") - structure InvalidThingException { - message: String + """.letIf(withAwsQueryError) { + it + + """ + @awsQueryError( + code: "InvalidThing", + httpResponseCode: 400, + ) + """ + }.let { + it + + """ + @error("client") + structure InvalidThingException { + message: String + } + """ } - """.asSmithyModel() + } + @Test + fun `aws-query-compatible json with aws query error should allow for retrieving error code and type from custom header`() { + val model = + (prologue + awsQueryCompatibleTrait + awsjson10Trait + testService()).asSmithyModel( + smithyVersion = "2", + ) clientIntegrationTest(model) { context, rustCrate -> - val operation: OperationShape = context.model.lookup("test#SomeOperation") - rustCrate.withModule(context.symbolProvider.moduleForShape(operation)) { - rustTemplate( - """ - ##[cfg(test)] - ##[#{tokio}::test] - async fn should_parse_code_and_type_fields() { - use aws_smithy_types::body::SdkBody; - - let response = |_: http::Request| { + rustCrate.testModule { + tokioTest("should_parse_code_and_type_fields") { + rustTemplate( + """ + let response = |_: http::Request<#{SdkBody}>| { http::Response::builder() .header( "x-amzn-query-error", @@ -69,7 +82,7 @@ class AwsQueryCompatibleTest { ) .status(400) .body( - SdkBody::from( + #{SdkBody}::from( r##"{ "__type": "com.amazonaws.sqs##QueueDoesNotExist", "message": "Some user-visible message" @@ -86,17 +99,18 @@ class AwsQueryCompatibleTest { ); let error = dbg!(client.some_operation().send().await).err().unwrap().into_service_error(); assert_eq!( - Some("AWS.SimpleQueueService.NonExistentQueue"), + #{Some}("AWS.SimpleQueueService.NonExistentQueue"), error.meta().code(), ); - assert_eq!(Some("Sender"), error.meta().extra("type")); - } - """, - "infallible_client_fn" to - CargoDependency.smithyRuntimeTestUtil(context.runtimeConfig) - .toType().resolve("client::http::test_util::infallible_client_fn"), - "tokio" to CargoDependency.Tokio.toType(), - ) + assert_eq!(#{Some}("Sender"), error.meta().extra("type")); + """, + *RuntimeType.preludeScope, + "SdkBody" to RuntimeType.sdkBody(context.runtimeConfig), + "infallible_client_fn" to + CargoDependency.smithyRuntimeTestUtil(context.runtimeConfig) + .toType().resolve("client::http::test_util::infallible_client_fn"), + ) + } } } } @@ -104,50 +118,19 @@ class AwsQueryCompatibleTest { @Test fun `aws-query-compatible json without aws query error should allow for retrieving error code from payload`() { val model = - """ - namespace test - use aws.protocols#awsJson1_0 - use aws.protocols#awsQueryCompatible - - @awsQueryCompatible - @awsJson1_0 - service TestService { - version: "2023-02-20", - operations: [SomeOperation] - } - - operation SomeOperation { - input: SomeOperationInputOutput, - output: SomeOperationInputOutput, - errors: [InvalidThingException], - } - - structure SomeOperationInputOutput { - a: String, - b: Integer - } - - @error("client") - structure InvalidThingException { - message: String - } - """.asSmithyModel() - + (prologue + awsQueryCompatibleTrait + awsjson10Trait + testService(withAwsQueryError = false)).asSmithyModel( + smithyVersion = "2", + ) clientIntegrationTest(model) { context, rustCrate -> - val operation: OperationShape = context.model.lookup("test#SomeOperation") - rustCrate.withModule(context.symbolProvider.moduleForShape(operation)) { - rustTemplate( - """ - ##[cfg(test)] - ##[#{tokio}::test] - async fn should_parse_code_from_payload() { - use aws_smithy_types::body::SdkBody; - - let response = |_: http::Request| { + rustCrate.testModule { + tokioTest("should_parse_code_from_payload") { + rustTemplate( + """ + let response = |_: http::Request<#{SdkBody}>| { http::Response::builder() .status(400) .body( - SdkBody::from( + #{SdkBody}::from( r##"{ "__type": "com.amazonaws.sqs##QueueDoesNotExist", "message": "Some user-visible message" @@ -163,15 +146,45 @@ class AwsQueryCompatibleTest { .build() ); let error = dbg!(client.some_operation().send().await).err().unwrap().into_service_error(); - assert_eq!(Some("QueueDoesNotExist"), error.meta().code()); - assert_eq!(None, error.meta().extra("type")); - } - """, - "infallible_client_fn" to - CargoDependency.smithyRuntimeTestUtil(context.runtimeConfig) - .toType().resolve("client::http::test_util::infallible_client_fn"), - "tokio" to CargoDependency.Tokio.toType(), - ) + assert_eq!(#{Some}("QueueDoesNotExist"), error.meta().code()); + assert_eq!(#{None}, error.meta().extra("type")); + """, + *RuntimeType.preludeScope, + "SdkBody" to RuntimeType.sdkBody(context.runtimeConfig), + "infallible_client_fn" to + CargoDependency.smithyRuntimeTestUtil(context.runtimeConfig) + .toType().resolve("client::http::test_util::infallible_client_fn"), + ) + } + } + } + } + + @Test + fun `request header should include x-amzn-query-mode when the service has the awsQueryCompatible trait`() { + val model = + (prologue + awsQueryCompatibleTrait + awsjson10Trait + testService()).asSmithyModel( + smithyVersion = "2", + ) + clientIntegrationTest(model) { context, rustCrate -> + rustCrate.testModule { + tokioTest("test_request_header_should_include_x_amzn_query_mode") { + rustTemplate( + """ + let (http_client, rx) = #{capture_request}(#{None}); + let config = crate::Config::builder() + .http_client(http_client) + .endpoint_url("http://localhost:1234/SomeOperation") + .build(); + let client = crate::Client::from_conf(config); + let _ = dbg!(client.some_operation().send().await); + let request = rx.expect_request(); + assert_eq!("true", request.headers().get("x-amzn-query-mode").unwrap()); + """, + *RuntimeType.preludeScope, + "capture_request" to RuntimeType.captureRequest(context.runtimeConfig), + ) + } } } } diff --git a/codegen-core/common-test-models/aws-json-query-compat.smithy b/codegen-core/common-test-models/aws-json-query-compat.smithy index b3e1cf0375..49cf71934d 100644 --- a/codegen-core/common-test-models/aws-json-query-compat.smithy +++ b/codegen-core/common-test-models/aws-json-query-compat.smithy @@ -24,8 +24,10 @@ service QueryCompatService { params: { message: "hello!" }, - headers: { "x-amz-target": "QueryCompatService.Operation"} - + headers: { + "x-amz-target": "QueryCompatService.Operation", + "x-amzn-query-mode": "true", + } } ]) operation Operation { diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQueryCompatible.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQueryCompatible.kt index 4cc4a2fa14..79c962a001 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQueryCompatible.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQueryCompatible.kt @@ -97,5 +97,8 @@ class AwsQueryCompatible( awsJson.parseEventStreamErrorMetadata(operationShape) override fun additionalRequestHeaders(operationShape: OperationShape): List> = - listOf("x-amz-target" to "${codegenContext.serviceShape.id.name}.${operationShape.id.name}") + listOf( + "x-amz-target" to "${codegenContext.serviceShape.id.name}.${operationShape.id.name}", + "x-amzn-query-mode" to "true", + ) }