-
Notifications
You must be signed in to change notification settings - Fork 189
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for the awsQueryCompatible trait (#2398)
* Add support for the awsQueryCompatible trait This commit adds support for the awsQueryCompatible trait. This allows services already supporting custom error codes through the AWS Query protocol with the awsQueryError trait to continue supporting them after the services switch to the AWS JSON 1.0 protocol. * Add copyright header * Fix clippy warning for clippy::manual-map * Update CHANGELOG.next.toml * Update CHANGELOG.next.toml * Update CHANGELOG.next.toml * Remove unused variables from `errorScope` This commit addresses #2398 (comment) * Reorder arguments for test verification This commit addresses #2398 (comment) --------- Co-authored-by: Yuki Saito <[email protected]>
- Loading branch information
Showing
8 changed files
with
416 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
152 changes: 152 additions & 0 deletions
152
...lin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryCompatibleTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package software.amazon.smithy.rust.codegen.client.smithy.protocols | ||
|
||
import org.junit.jupiter.api.Test | ||
import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest | ||
import software.amazon.smithy.rust.codegen.core.rustlang.rust | ||
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel | ||
import software.amazon.smithy.rust.codegen.core.testutil.integrationTest | ||
|
||
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 = """ | ||
namespace test | ||
use aws.protocols#awsJson1_0 | ||
use aws.protocols#awsQueryCompatible | ||
use aws.protocols#awsQueryError | ||
@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 | ||
} | ||
@awsQueryError( | ||
code: "InvalidThing", | ||
httpResponseCode: 400, | ||
) | ||
@error("client") | ||
structure InvalidThingException { | ||
message: String | ||
} | ||
""".asSmithyModel() | ||
|
||
clientIntegrationTest(model) { clientCodegenContext, rustCrate -> | ||
val moduleName = clientCodegenContext.moduleUseName() | ||
rustCrate.integrationTest("should_parse_code_and_type_fields") { | ||
rust( | ||
""" | ||
##[test] | ||
fn should_parse_code_and_type_fields() { | ||
use aws_smithy_http::response::ParseStrictResponse; | ||
let response = http::Response::builder() | ||
.header( | ||
"x-amzn-query-error", | ||
http::HeaderValue::from_static("AWS.SimpleQueueService.NonExistentQueue;Sender"), | ||
) | ||
.status(400) | ||
.body( | ||
r##"{ | ||
"__type": "com.amazonaws.sqs##QueueDoesNotExist", | ||
"message": "Some user-visible message" | ||
}"##, | ||
) | ||
.unwrap(); | ||
let some_operation = $moduleName::operation::SomeOperation::new(); | ||
let error = some_operation | ||
.parse(&response.map(bytes::Bytes::from)) | ||
.err() | ||
.unwrap(); | ||
assert_eq!( | ||
Some("AWS.SimpleQueueService.NonExistentQueue"), | ||
error.meta().code(), | ||
); | ||
assert_eq!(Some("Sender"), error.meta().extra("type")); | ||
} | ||
""", | ||
) | ||
} | ||
} | ||
} | ||
|
||
@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() | ||
|
||
clientIntegrationTest(model) { clientCodegenContext, rustCrate -> | ||
val moduleName = clientCodegenContext.moduleUseName() | ||
rustCrate.integrationTest("should_parse_code_from_payload") { | ||
rust( | ||
""" | ||
##[test] | ||
fn should_parse_code_from_payload() { | ||
use aws_smithy_http::response::ParseStrictResponse; | ||
let response = http::Response::builder() | ||
.status(400) | ||
.body( | ||
r##"{ | ||
"__type": "com.amazonaws.sqs##QueueDoesNotExist", | ||
"message": "Some user-visible message" | ||
}"##, | ||
) | ||
.unwrap(); | ||
let some_operation = $moduleName::operation::SomeOperation::new(); | ||
let error = some_operation | ||
.parse(&response.map(bytes::Bytes::from)) | ||
.err() | ||
.unwrap(); | ||
assert_eq!(Some("QueueDoesNotExist"), error.meta().code()); | ||
assert_eq!(None, error.meta().extra("type")); | ||
} | ||
""", | ||
) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
...in/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQueryCompatible.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package software.amazon.smithy.rust.codegen.core.smithy.protocols | ||
|
||
import software.amazon.smithy.model.shapes.OperationShape | ||
import software.amazon.smithy.model.shapes.ToShapeId | ||
import software.amazon.smithy.model.traits.HttpTrait | ||
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency | ||
import software.amazon.smithy.rust.codegen.core.rustlang.RustModule | ||
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate | ||
import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext | ||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType | ||
import software.amazon.smithy.rust.codegen.core.smithy.protocols.parse.StructuredDataParserGenerator | ||
import software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize.StructuredDataSerializerGenerator | ||
|
||
class AwsQueryCompatibleHttpBindingResolver( | ||
private val awsQueryBindingResolver: AwsQueryBindingResolver, | ||
private val awsJsonHttpBindingResolver: AwsJsonHttpBindingResolver, | ||
) : HttpBindingResolver { | ||
override fun httpTrait(operationShape: OperationShape): HttpTrait = | ||
awsJsonHttpBindingResolver.httpTrait(operationShape) | ||
|
||
override fun requestBindings(operationShape: OperationShape): List<HttpBindingDescriptor> = | ||
awsJsonHttpBindingResolver.requestBindings(operationShape) | ||
|
||
override fun responseBindings(operationShape: OperationShape): List<HttpBindingDescriptor> = | ||
awsJsonHttpBindingResolver.responseBindings(operationShape) | ||
|
||
override fun errorResponseBindings(errorShape: ToShapeId): List<HttpBindingDescriptor> = | ||
awsJsonHttpBindingResolver.errorResponseBindings(errorShape) | ||
|
||
override fun errorCode(errorShape: ToShapeId): String = | ||
awsQueryBindingResolver.errorCode(errorShape) | ||
|
||
override fun requestContentType(operationShape: OperationShape): String = | ||
awsJsonHttpBindingResolver.requestContentType(operationShape) | ||
|
||
override fun responseContentType(operationShape: OperationShape): String = | ||
awsJsonHttpBindingResolver.requestContentType(operationShape) | ||
} | ||
|
||
class AwsQueryCompatible( | ||
val codegenContext: CodegenContext, | ||
private val awsJson: AwsJson, | ||
) : Protocol { | ||
private val runtimeConfig = codegenContext.runtimeConfig | ||
private val errorScope = arrayOf( | ||
"Bytes" to RuntimeType.Bytes, | ||
"ErrorMetadataBuilder" to RuntimeType.errorMetadataBuilder(runtimeConfig), | ||
"JsonError" to CargoDependency.smithyJson(runtimeConfig).toType() | ||
.resolve("deserialize::error::DeserializeError"), | ||
"Response" to RuntimeType.Http.resolve("Response"), | ||
"json_errors" to RuntimeType.jsonErrors(runtimeConfig), | ||
"aws_query_compatible_errors" to RuntimeType.awsQueryCompatibleErrors(runtimeConfig), | ||
) | ||
private val jsonDeserModule = RustModule.private("json_deser") | ||
|
||
override val httpBindingResolver: HttpBindingResolver = | ||
AwsQueryCompatibleHttpBindingResolver( | ||
AwsQueryBindingResolver(codegenContext.model), | ||
AwsJsonHttpBindingResolver(codegenContext.model, awsJson.version), | ||
) | ||
|
||
override val defaultTimestampFormat = awsJson.defaultTimestampFormat | ||
|
||
override fun structuredDataParser(operationShape: OperationShape): StructuredDataParserGenerator = | ||
awsJson.structuredDataParser(operationShape) | ||
|
||
override fun structuredDataSerializer(operationShape: OperationShape): StructuredDataSerializerGenerator = | ||
awsJson.structuredDataSerializer(operationShape) | ||
|
||
override fun parseHttpErrorMetadata(operationShape: OperationShape): RuntimeType = | ||
RuntimeType.forInlineFun("parse_http_error_metadata", jsonDeserModule) { | ||
rustTemplate( | ||
""" | ||
pub fn parse_http_error_metadata(response: &#{Response}<#{Bytes}>) -> Result<#{ErrorMetadataBuilder}, #{JsonError}> { | ||
let mut builder = | ||
#{json_errors}::parse_error_metadata(response.body(), response.headers())?; | ||
if let Some((error_code, error_type)) = | ||
#{aws_query_compatible_errors}::parse_aws_query_compatible_error(response.headers()) | ||
{ | ||
builder = builder.code(error_code); | ||
builder = builder.custom("type", error_type); | ||
} | ||
Ok(builder) | ||
} | ||
""", | ||
*errorScope, | ||
) | ||
} | ||
|
||
override fun parseEventStreamErrorMetadata(operationShape: OperationShape): RuntimeType = | ||
awsJson.parseEventStreamErrorMetadata(operationShape) | ||
} |
Oops, something went wrong.