Implement `StdError::source()` for Error enum (#2564)

## Motivation and Context
This is an attempt at fixing
https://github.com/awslabs/aws-sdk-rust/issues/784.

The service-level `Error` enum implements `std::error::Error` but does
not implement its `source()` method. This means that an error library
like `anyhow` or `eyre` won't be able to display the root cause of an
error, which is especially problematic for the `Unhandled` variant.

## Description
I modified `ServiceErrorGenerator` in the `codegen-client` crate and
replaced the line that output `impl std::error::Error for Error {}` with
an impl block that implements the `source()` method by delegating to the
inner error structure.

## Testing
I've added a simple unit test to `ServiceErrorGeneratorTest`.

## 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:
Antoine Büsch 2023-04-17 23:51:01 +10:00 committed by GitHub
parent 35f2f27a83
commit fc63800f6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 77 additions and 32 deletions

View File

@ -11,6 +11,18 @@
# meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"}
# author = "rcoh"
[[aws-sdk-rust]]
message = "Implement std::error::Error#source() properly for the service meta Error enum"
references = ["aws-sdk-rust#784"]
meta = { "breaking" = false, "tada" = false, "bug" = false }
author = "abusch"
[[smithy-rs]]
message = "Implement std::error::Error#source() properly for the service meta Error enum"
references = ["aws-sdk-rust#784"]
meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client"}
author = "abusch"
[[aws-sdk-rust]]
message = "The outputs for event stream operations (for example, S3's SelectObjectContent) now implement the `Sync` auto-trait."
references = ["smithy-rs#2496"]

View File

@ -80,7 +80,16 @@ class ServiceErrorGenerator(
errors.map { it.id },
)
}
rust("impl #T for Error {}", RuntimeType.StdError)
rustBlock("impl #T for Error", RuntimeType.StdError) {
rustBlock("fn source(&self) -> std::option::Option<&(dyn #T + 'static)>", RuntimeType.StdError) {
rustBlock("match self") {
allErrors.forEach {
rust("Error::${symbolProvider.toSymbol(it).name}(inner) => inner.source(),")
}
rust("Error::Unhandled(inner) => inner.source()")
}
}
}
writeCustomizations(customizations, ErrorSection.ServiceErrorAdditionalTraitImpls(allErrors))
}
crate.lib { rust("pub use error_meta::Error;") }

View File

@ -6,45 +6,48 @@
package software.amazon.smithy.rust.codegen.client.smithy.generators.error
import org.junit.jupiter.api.Test
import software.amazon.smithy.model.shapes.StructureShape
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
import software.amazon.smithy.rust.codegen.core.testutil.unitTest
import software.amazon.smithy.rust.codegen.core.util.lookup
internal class ServiceErrorGeneratorTest {
private val model = """
namespace com.example
use aws.protocols#restJson1
@restJson1
service HelloService {
operations: [SayHello],
version: "1"
}
@http(uri: "/", method: "POST")
operation SayHello {
input: EmptyStruct,
output: EmptyStruct,
errors: [SorryBusy, CanYouRepeatThat, MeDeprecated]
}
structure EmptyStruct { }
@error("server")
structure SorryBusy { }
@error("client")
structure CanYouRepeatThat { }
@error("client")
@deprecated
structure MeDeprecated { }
""".asSmithyModel()
@Test
fun `top level errors are send + sync`() {
val model = """
namespace com.example
use aws.protocols#restJson1
@restJson1
service HelloService {
operations: [SayHello],
version: "1"
}
@http(uri: "/", method: "POST")
operation SayHello {
input: EmptyStruct,
output: EmptyStruct,
errors: [SorryBusy, CanYouRepeatThat, MeDeprecated]
}
structure EmptyStruct { }
@error("server")
structure SorryBusy { }
@error("client")
structure CanYouRepeatThat { }
@error("client")
@deprecated
structure MeDeprecated { }
""".asSmithyModel()
clientIntegrationTest(model) { codegenContext, rustCrate ->
rustCrate.integrationTest("validate_errors") {
rust(
@ -60,4 +63,25 @@ internal class ServiceErrorGeneratorTest {
}
}
}
@Test
fun `generates combined error enums`() {
clientIntegrationTest(model) { _, rustCrate ->
rustCrate.moduleFor(model.lookup<StructureShape>("com.example#CanYouRepeatThat")) {
unitTest(
name = "generates_combined_error_enums",
test = """
use std::error::Error as StdError;
use crate::Error;
use crate::operation::say_hello::SayHelloError;
// Unhandled variants properly delegate source.
let error = Error::from(SayHelloError::unhandled("some other error"));
let source = error.source().expect("source should not be None");
assert_eq!(format!("{}", source), "some other error");
""",
)
}
}
}
}