mirror of https://github.com/smithy-lang/smithy-rs
Revise unhandled error variant according to RFC-39 (#3191)
This PR implements [RFC-39](https://github.com/smithy-lang/smithy-rs/blob/main/design/src/rfcs/rfc0039_forward_compatible_errors.md) with a couple slight deviations: - No `introspect` method is added since `Error` already implements `ProvideErrorMetadata`. - The same opaqueness and deprecation pointer is applied to the enum unknown variant for consistency. ---- _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:
parent
c0f72fbfe8
commit
c830caa281
|
@ -119,3 +119,61 @@ message = "Remove deprecated error kind type aliases."
|
|||
references = ["smithy-rs#3189"]
|
||||
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
|
||||
author = "jdisanti"
|
||||
|
||||
[[aws-sdk-rust]]
|
||||
message = """
|
||||
Unhandled errors have been made opaque to ensure code is written in a future-proof manner. Where previously, you
|
||||
might have:
|
||||
```rust
|
||||
match service_error.err() {
|
||||
GetStorageError::StorageAccessNotAuthorized(_) => { /* ... */ }
|
||||
GetStorageError::Unhandled(unhandled) if unhandled.code() == Some("SomeUnmodeledErrorCode") {
|
||||
// unhandled error handling
|
||||
}
|
||||
_ => { /* ... */ }
|
||||
}
|
||||
```
|
||||
It should now look as follows:
|
||||
```rust
|
||||
match service_error.err() {
|
||||
GetStorageError::StorageAccessNotAuthorized(_) => { /* ... */ }
|
||||
err if err.code() == Some("SomeUnmodeledErrorCode") {
|
||||
// unhandled error handling
|
||||
}
|
||||
_ => { /* ... */ }
|
||||
}
|
||||
```
|
||||
The `Unhandled` variant should never be referenced directly.
|
||||
"""
|
||||
references = ["smithy-rs#3191"]
|
||||
meta = { "breaking" = true, "tada" = false, "bug" = false }
|
||||
author = "jdisanti"
|
||||
|
||||
[[smithy-rs]]
|
||||
message = """
|
||||
Unhandled errors have been made opaque to ensure code is written in a future-proof manner. Where previously, you
|
||||
might have:
|
||||
```rust
|
||||
match service_error.err() {
|
||||
GetStorageError::StorageAccessNotAuthorized(_) => { /* ... */ }
|
||||
GetStorageError::Unhandled(unhandled) if unhandled.code() == Some("SomeUnmodeledErrorCode") {
|
||||
// unhandled error handling
|
||||
}
|
||||
_ => { /* ... */ }
|
||||
}
|
||||
```
|
||||
It should now look as follows:
|
||||
```rust
|
||||
match service_error.err() {
|
||||
GetStorageError::StorageAccessNotAuthorized(_) => { /* ... */ }
|
||||
err if err.code() == Some("SomeUnmodeledErrorCode") {
|
||||
// unhandled error handling
|
||||
}
|
||||
_ => { /* ... */ }
|
||||
}
|
||||
```
|
||||
The `Unhandled` variant should never be referenced directly.
|
||||
"""
|
||||
references = ["smithy-rs#3191"]
|
||||
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
|
||||
author = "jdisanti"
|
||||
|
|
|
@ -8,6 +8,7 @@ use aws_smithy_runtime_api::http::{Headers, Response};
|
|||
use aws_smithy_types::error::metadata::{
|
||||
Builder as ErrorMetadataBuilder, ErrorMetadata, ProvideErrorMetadata,
|
||||
};
|
||||
#[allow(deprecated)]
|
||||
use aws_smithy_types::error::Unhandled;
|
||||
|
||||
const EXTENDED_REQUEST_ID: &str = "s3_extended_request_id";
|
||||
|
@ -36,6 +37,7 @@ impl RequestIdExt for ErrorMetadata {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl RequestIdExt for Unhandled {
|
||||
fn extended_request_id(&self) -> Option<&str> {
|
||||
self.meta().extended_request_id()
|
||||
|
|
|
@ -11,6 +11,8 @@ use aws_smithy_runtime_api::http::Response;
|
|||
use aws_smithy_types::error::metadata::{
|
||||
Builder as ErrorMetadataBuilder, ErrorMetadata, ProvideErrorMetadata,
|
||||
};
|
||||
|
||||
#[allow(deprecated)]
|
||||
use aws_smithy_types::error::Unhandled;
|
||||
|
||||
/// Constant for the [`ErrorMetadata`] extra field that contains the request ID
|
||||
|
@ -38,6 +40,7 @@ impl RequestId for ErrorMetadata {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl RequestId for Unhandled {
|
||||
fn request_id(&self) -> Option<&str> {
|
||||
self.meta().request_id()
|
||||
|
|
|
@ -146,7 +146,7 @@ abstract class BaseRequestIdDecorator : ClientCodegenDecorator {
|
|||
val sym = codegenContext.symbolProvider.toSymbol(error)
|
||||
rust("Self::${sym.name}(e) => #T,", wrapped)
|
||||
}
|
||||
rust("Self::Unhandled(e) => e.$accessorFunctionName(),")
|
||||
rust("Self::Unhandled(e) => e.meta.$accessorFunctionName(),")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use aws_sdk_lambda::operation::RequestId;
|
|||
use aws_sdk_lambda::{Client, Config};
|
||||
use aws_smithy_runtime::client::http::test_util::infallible_client_fn;
|
||||
|
||||
#[allow(deprecated)]
|
||||
async fn run_test(
|
||||
response: impl Fn() -> http::Response<&'static str> + Send + Sync + 'static,
|
||||
expect_error: bool,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
use aws_credential_types::provider::SharedCredentialsProvider;
|
||||
use aws_sdk_s3::config::{Credentials, Region};
|
||||
use aws_sdk_s3::operation::list_objects_v2::ListObjectsV2Error;
|
||||
use aws_sdk_s3::{Client, Config};
|
||||
use aws_smithy_runtime::client::http::test_util::capture_request;
|
||||
|
||||
|
@ -58,8 +59,8 @@ async fn test_s3_signer_query_string_with_all_valid_chars() {
|
|||
// test must be run against an actual bucket so we `ignore` it unless the runner specifically requests it
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
#[allow(deprecated)]
|
||||
async fn test_query_strings_are_correctly_encoded() {
|
||||
use aws_sdk_s3::operation::list_objects_v2::ListObjectsV2Error;
|
||||
use aws_smithy_runtime_api::client::result::SdkError;
|
||||
|
||||
tracing_subscriber::fmt::init();
|
||||
|
@ -80,22 +81,19 @@ async fn test_query_strings_are_correctly_encoded() {
|
|||
.send()
|
||||
.await;
|
||||
if let Err(SdkError::ServiceError(context)) = res {
|
||||
match context.err() {
|
||||
ListObjectsV2Error::Unhandled(e)
|
||||
if e.to_string().contains("SignatureDoesNotMatch") =>
|
||||
{
|
||||
chars_that_break_signing.push(byte);
|
||||
}
|
||||
ListObjectsV2Error::Unhandled(e) if e.to_string().contains("InvalidUri") => {
|
||||
chars_that_break_uri_parsing.push(byte);
|
||||
}
|
||||
ListObjectsV2Error::Unhandled(e) if e.to_string().contains("InvalidArgument") => {
|
||||
chars_that_are_invalid_arguments.push(byte);
|
||||
}
|
||||
ListObjectsV2Error::Unhandled(e) if e.to_string().contains("InvalidToken") => {
|
||||
panic!("refresh your credentials and run this test again");
|
||||
}
|
||||
e => todo!("unexpected error: {:?}", e),
|
||||
let err = context.err();
|
||||
let msg = err.to_string();
|
||||
let unhandled = matches!(err, ListObjectsV2Error::Unhandled(_));
|
||||
if unhandled && msg.contains("SignatureDoesNotMatch") {
|
||||
chars_that_break_signing.push(byte);
|
||||
} else if unhandled && msg.to_string().contains("InvalidUri") {
|
||||
chars_that_break_uri_parsing.push(byte);
|
||||
} else if unhandled && msg.to_string().contains("InvalidArgument") {
|
||||
chars_that_are_invalid_arguments.push(byte);
|
||||
} else if unhandled && msg.to_string().contains("InvalidToken") {
|
||||
panic!("refresh your credentials and run this test again");
|
||||
} else {
|
||||
todo!("unexpected error: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ async fn get_request_id_from_modeled_error() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[allow(deprecated)]
|
||||
async fn get_request_id_from_unmodeled_error() {
|
||||
let (http_client, request) = capture_request(Some(
|
||||
http::Response::builder()
|
||||
|
|
|
@ -10,6 +10,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
|
|||
import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustModule
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
|
||||
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.docs
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.rust
|
||||
|
@ -75,12 +76,39 @@ data class InfallibleEnumType(
|
|||
)
|
||||
}
|
||||
|
||||
override fun additionalEnumImpls(context: EnumGeneratorContext): Writable = writable {
|
||||
// `try_parse` isn't needed for unnamed enums
|
||||
if (context.enumTrait.hasNames()) {
|
||||
rustTemplate(
|
||||
"""
|
||||
impl ${context.enumName} {
|
||||
/// Parses the enum value while disallowing unknown variants.
|
||||
///
|
||||
/// Unknown variants will result in an error.
|
||||
pub fn try_parse(value: &str) -> #{Result}<Self, #{UnknownVariantError}> {
|
||||
match Self::from(value) {
|
||||
##[allow(deprecated)]
|
||||
Self::Unknown(_) => #{Err}(#{UnknownVariantError}::new(value)),
|
||||
known => Ok(known),
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
*preludeScope,
|
||||
"UnknownVariantError" to unknownVariantError(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun additionalDocs(context: EnumGeneratorContext): Writable = writable {
|
||||
renderForwardCompatibilityNote(context.enumName, context.sortedMembers, UnknownVariant, UnknownVariantValue)
|
||||
}
|
||||
|
||||
override fun additionalEnumMembers(context: EnumGeneratorContext): Writable = writable {
|
||||
docs("`$UnknownVariant` contains new variants that have been added since this code was generated.")
|
||||
rust(
|
||||
"""##[deprecated(note = "Don't directly match on `$UnknownVariant`. See the docs on this enum for the correct way to handle unknown variants.")]""",
|
||||
)
|
||||
rust("$UnknownVariant(#T)", unknownVariantValue(context))
|
||||
}
|
||||
|
||||
|
@ -93,10 +121,9 @@ data class InfallibleEnumType(
|
|||
docs(
|
||||
"""
|
||||
Opaque struct used as inner data for the `Unknown` variant defined in enums in
|
||||
the crate
|
||||
the crate.
|
||||
|
||||
While this is not intended to be used directly, it is marked as `pub` because it is
|
||||
part of the enums that are public interface.
|
||||
This is not intended to be used directly.
|
||||
""".trimIndent(),
|
||||
)
|
||||
context.enumMeta.render(this)
|
||||
|
@ -174,5 +201,35 @@ class ClientEnumGenerator(codegenContext: ClientCodegenContext, shape: StringSha
|
|||
codegenContext.model,
|
||||
codegenContext.symbolProvider,
|
||||
shape,
|
||||
InfallibleEnumType(ClientRustModule.primitives),
|
||||
InfallibleEnumType(
|
||||
RustModule.new(
|
||||
"sealed_enum_unknown",
|
||||
visibility = Visibility.PUBCRATE,
|
||||
parent = ClientRustModule.primitives,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private fun unknownVariantError(): RuntimeType = RuntimeType.forInlineFun("UnknownVariantError", ClientRustModule.Error) {
|
||||
rustTemplate(
|
||||
"""
|
||||
/// The given enum value failed to parse since it is not a known value.
|
||||
##[derive(Debug)]
|
||||
pub struct UnknownVariantError {
|
||||
value: #{String},
|
||||
}
|
||||
impl UnknownVariantError {
|
||||
pub(crate) fn new(value: impl #{Into}<#{String}>) -> Self {
|
||||
Self { value: value.into() }
|
||||
}
|
||||
}
|
||||
impl ::std::fmt::Display for UnknownVariantError {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> #{Result}<(), ::std::fmt::Error> {
|
||||
write!(f, "unknown enum variant: '{}'", self.value)
|
||||
}
|
||||
}
|
||||
impl ::std::error::Error for UnknownVariantError {}
|
||||
""",
|
||||
*preludeScope,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ 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.RuntimeType.Companion.errorMetadata
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.unhandledError
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.customize.Section
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.customize.writeCustomizations
|
||||
|
@ -83,12 +82,14 @@ class OperationErrorGenerator(
|
|||
val errorVariantSymbol = symbolProvider.toSymbol(errorVariant)
|
||||
write("${errorVariantSymbol.name}(#T),", errorVariantSymbol)
|
||||
}
|
||||
rust(
|
||||
rustTemplate(
|
||||
"""
|
||||
/// An unexpected error occurred (e.g., invalid JSON returned by the service or an unknown error code).
|
||||
Unhandled(#T),
|
||||
#{deprecation}
|
||||
Unhandled(#{Unhandled}),
|
||||
""",
|
||||
unhandledError(runtimeConfig),
|
||||
"deprecation" to writable { renderUnhandledErrorDeprecation(runtimeConfig, errorSymbol.name) },
|
||||
"Unhandled" to unhandledError(runtimeConfig),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -114,15 +115,9 @@ class OperationErrorGenerator(
|
|||
"StdError" to RuntimeType.StdError,
|
||||
"ErrorMeta" to errorMetadata,
|
||||
) {
|
||||
rust(
|
||||
"""
|
||||
Self::Unhandled({
|
||||
let mut builder = #T::builder().source(source);
|
||||
builder.set_meta(meta);
|
||||
builder.build()
|
||||
})
|
||||
""",
|
||||
unhandledError(runtimeConfig),
|
||||
rustTemplate(
|
||||
"""Self::Unhandled(#{Unhandled} { source, meta: meta.unwrap_or_default() })""",
|
||||
"Unhandled" to unhandledError(runtimeConfig),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -131,8 +126,23 @@ class OperationErrorGenerator(
|
|||
private fun RustWriter.renderImplDisplay(errorSymbol: Symbol, errors: List<StructureShape>) {
|
||||
rustBlock("impl #T for ${errorSymbol.name}", RuntimeType.Display) {
|
||||
rustBlock("fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result") {
|
||||
delegateToVariants(errors) {
|
||||
writable { rust("_inner.fmt(f)") }
|
||||
delegateToVariants(errors) { variantMatch ->
|
||||
when (variantMatch) {
|
||||
is VariantMatch.Unhandled -> writable {
|
||||
rustTemplate(
|
||||
"""
|
||||
if let #{Some}(code) = #{ProvideErrorMetadata}::code(self) {
|
||||
write!(f, "unhandled error ({code})")
|
||||
} else {
|
||||
f.write_str("unhandled error")
|
||||
}
|
||||
""",
|
||||
*preludeScope,
|
||||
"ProvideErrorMetadata" to RuntimeType.provideErrorMetadataTrait(runtimeConfig),
|
||||
)
|
||||
}
|
||||
is VariantMatch.Modeled -> writable { rust("_inner.fmt(f)") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -142,8 +152,13 @@ class OperationErrorGenerator(
|
|||
val errorMetadataTrait = RuntimeType.provideErrorMetadataTrait(runtimeConfig)
|
||||
rustBlock("impl #T for ${errorSymbol.name}", errorMetadataTrait) {
|
||||
rustBlock("fn meta(&self) -> &#T", errorMetadata(runtimeConfig)) {
|
||||
delegateToVariants(errors) {
|
||||
writable { rust("#T::meta(_inner)", errorMetadataTrait) }
|
||||
delegateToVariants(errors) { variantMatch ->
|
||||
writable {
|
||||
when (variantMatch) {
|
||||
is VariantMatch.Unhandled -> rust("&_inner.meta")
|
||||
is VariantMatch.Modeled -> rust("#T::meta(_inner)", errorMetadataTrait)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,16 +204,16 @@ class OperationErrorGenerator(
|
|||
"""
|
||||
/// Creates the `${errorSymbol.name}::Unhandled` variant from any error type.
|
||||
pub fn unhandled(err: impl #{Into}<#{Box}<dyn #{StdError} + #{Send} + #{Sync} + 'static>>) -> Self {
|
||||
Self::Unhandled(#{Unhandled}::builder().source(err).build())
|
||||
Self::Unhandled(#{Unhandled} { source: err.into(), meta: #{Default}::default() })
|
||||
}
|
||||
|
||||
/// Creates the `${errorSymbol.name}::Unhandled` variant from a `#{error_metadata}`.
|
||||
pub fn generic(err: #{error_metadata}) -> Self {
|
||||
Self::Unhandled(#{Unhandled}::builder().source(err.clone()).meta(err).build())
|
||||
/// Creates the `${errorSymbol.name}::Unhandled` variant from an [`ErrorMetadata`](#{ErrorMetadata}).
|
||||
pub fn generic(err: #{ErrorMetadata}) -> Self {
|
||||
Self::Unhandled(#{Unhandled} { source: err.clone().into(), meta: err })
|
||||
}
|
||||
""",
|
||||
*preludeScope,
|
||||
"error_metadata" to errorMetadata,
|
||||
"ErrorMetadata" to errorMetadata,
|
||||
"StdError" to RuntimeType.StdError,
|
||||
"Unhandled" to unhandledError(runtimeConfig),
|
||||
)
|
||||
|
@ -209,13 +224,15 @@ class OperationErrorGenerator(
|
|||
""",
|
||||
)
|
||||
rustBlock("pub fn meta(&self) -> &#T", errorMetadata) {
|
||||
rust("use #T;", RuntimeType.provideErrorMetadataTrait(runtimeConfig))
|
||||
rustBlock("match self") {
|
||||
errors.forEach { error ->
|
||||
val errorVariantSymbol = symbolProvider.toSymbol(error)
|
||||
rust("Self::${errorVariantSymbol.name}(e) => e.meta(),")
|
||||
rustTemplate(
|
||||
"Self::${errorVariantSymbol.name}(e) => #{ProvideErrorMetadata}::meta(e),",
|
||||
"ProvideErrorMetadata" to RuntimeType.provideErrorMetadataTrait(runtimeConfig),
|
||||
)
|
||||
}
|
||||
rust("Self::Unhandled(e) => e.meta(),")
|
||||
rust("Self::Unhandled(e) => &e.meta,")
|
||||
}
|
||||
}
|
||||
errors.forEach { error ->
|
||||
|
@ -236,9 +253,14 @@ class OperationErrorGenerator(
|
|||
*preludeScope,
|
||||
"StdError" to RuntimeType.StdError,
|
||||
) {
|
||||
delegateToVariants(errors) {
|
||||
writable {
|
||||
rustTemplate("#{Some}(_inner)", *preludeScope)
|
||||
delegateToVariants(errors) { variantMatch ->
|
||||
when (variantMatch) {
|
||||
is VariantMatch.Unhandled -> writable {
|
||||
rustTemplate("#{Some}(&*_inner.source)", *preludeScope)
|
||||
}
|
||||
is VariantMatch.Modeled -> writable {
|
||||
rustTemplate("#{Some}(_inner)", *preludeScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import software.amazon.smithy.codegen.core.Symbol
|
|||
import software.amazon.smithy.model.shapes.OperationShape
|
||||
import software.amazon.smithy.model.shapes.ShapeId
|
||||
import software.amazon.smithy.model.shapes.StructureShape
|
||||
import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata
|
||||
import software.amazon.smithy.rust.codegen.core.rustlang.RustModule
|
||||
|
@ -24,9 +25,9 @@ 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.CodegenContext
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.CodegenTarget
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
|
||||
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.smithy.RuntimeType.Companion.unhandledError
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.customize.writeCustomizations
|
||||
import software.amazon.smithy.rust.codegen.core.smithy.generators.operationBuildError
|
||||
|
@ -91,7 +92,7 @@ class ServiceErrorGenerator(
|
|||
allErrors.forEach {
|
||||
rust("Error::${symbolProvider.toSymbol(it).name}(inner) => inner.source(),")
|
||||
}
|
||||
rust("Error::Unhandled(inner) => inner.source()")
|
||||
rustTemplate("Error::Unhandled(inner) => #{Some}(&*inner.source)", *preludeScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +108,17 @@ class ServiceErrorGenerator(
|
|||
allErrors.forEach {
|
||||
rust("Error::${symbolProvider.toSymbol(it).name}(inner) => inner.fmt(f),")
|
||||
}
|
||||
rust("Error::Unhandled(inner) => inner.fmt(f)")
|
||||
rustTemplate(
|
||||
"""
|
||||
Error::Unhandled(_) => if let #{Some}(code) = #{ProvideErrorMetadata}::code(self) {
|
||||
write!(f, "unhandled error ({code})")
|
||||
} else {
|
||||
f.write_str("unhandled error")
|
||||
}
|
||||
""",
|
||||
*preludeScope,
|
||||
"ProvideErrorMetadata" to RuntimeType.provideErrorMetadataTrait(codegenContext.runtimeConfig),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,11 +129,12 @@ class ServiceErrorGenerator(
|
|||
"""
|
||||
impl From<#{BuildError}> for Error {
|
||||
fn from(value: #{BuildError}) -> Self {
|
||||
Error::Unhandled(#{Unhandled}::builder().source(value).build())
|
||||
Error::Unhandled(#{Unhandled} { source: value.into(), meta: #{Default}::default() })
|
||||
}
|
||||
}
|
||||
|
||||
""",
|
||||
*preludeScope,
|
||||
"BuildError" to codegenContext.runtimeConfig.operationBuildError(),
|
||||
"Unhandled" to unhandledError(codegenContext.runtimeConfig),
|
||||
)
|
||||
|
@ -146,10 +158,10 @@ class ServiceErrorGenerator(
|
|||
rustTemplate(
|
||||
"""
|
||||
_ => Error::Unhandled(
|
||||
#{Unhandled}::builder()
|
||||
.meta(#{ProvideErrorMetadata}::meta(&err).clone())
|
||||
.source(err)
|
||||
.build()
|
||||
#{Unhandled} {
|
||||
meta: #{ProvideErrorMetadata}::meta(&err).clone(),
|
||||
source: err.into(),
|
||||
}
|
||||
),
|
||||
""",
|
||||
"Unhandled" to unhandledError(codegenContext.runtimeConfig),
|
||||
|
@ -187,7 +199,7 @@ class ServiceErrorGenerator(
|
|||
fn meta(&self) -> &#{ErrorMetadata} {
|
||||
match self {
|
||||
#{matchers}
|
||||
Self::Unhandled(inner) => inner.meta(),
|
||||
Self::Unhandled(inner) => &inner.meta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -220,7 +232,57 @@ class ServiceErrorGenerator(
|
|||
rust("${sym.name}(#T),", sym)
|
||||
}
|
||||
docs("An unexpected error occurred (e.g., invalid JSON returned by the service or an unknown error code).")
|
||||
renderUnhandledErrorDeprecation(codegenContext.runtimeConfig, "Error")
|
||||
rust("Unhandled(#T)", unhandledError(codegenContext.runtimeConfig))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unhandledError(rc: RuntimeConfig): RuntimeType = RuntimeType.forInlineFun(
|
||||
"Unhandled",
|
||||
// Place in a sealed module so that it can't be referenced at all
|
||||
RustModule.pubCrate("sealed_unhandled", ClientRustModule.Error),
|
||||
) {
|
||||
rustTemplate(
|
||||
"""
|
||||
/// This struct is not intended to be used.
|
||||
///
|
||||
/// This struct holds information about an unhandled error,
|
||||
/// but that information should be obtained by using the
|
||||
/// [`ProvideErrorMetadata`](#{ProvideErrorMetadata}) trait
|
||||
/// on the error type.
|
||||
///
|
||||
/// This struct intentionally doesn't yield any useful information itself.
|
||||
#{deprecation}
|
||||
##[derive(Debug)]
|
||||
pub struct Unhandled {
|
||||
pub(crate) source: #{BoxError},
|
||||
pub(crate) meta: #{ErrorMetadata},
|
||||
}
|
||||
""",
|
||||
"BoxError" to RuntimeType.smithyRuntimeApi(rc).resolve("box_error::BoxError"),
|
||||
"deprecation" to writable { renderUnhandledErrorDeprecation(rc) },
|
||||
"ErrorMetadata" to RuntimeType.smithyTypes(rc).resolve("error::metadata::ErrorMetadata"),
|
||||
"ProvideErrorMetadata" to RuntimeType.smithyTypes(rc).resolve("error::metadata::ProvideErrorMetadata"),
|
||||
)
|
||||
}
|
||||
|
||||
fun RustWriter.renderUnhandledErrorDeprecation(rc: RuntimeConfig, errorName: String? = null) {
|
||||
val link = if (errorName != null) {
|
||||
"##impl-ProvideErrorMetadata-for-$errorName"
|
||||
} else {
|
||||
"#{ProvideErrorMetadata}"
|
||||
}
|
||||
val message = """
|
||||
Matching `Unhandled` directly is not forwards compatible. Instead, match using a
|
||||
variable wildcard pattern and check `.code()`:<br/>
|
||||
`err if err.code() == Some("SpecificExceptionCode") => { /* handle the error */ }`<br/>
|
||||
See [`ProvideErrorMetadata`]($link) for what information is available for the error.
|
||||
""".trimIndent()
|
||||
// `.dq()` doesn't quite do what we want here since we actually want a Rust multi-line string
|
||||
val messageEscaped = message.replace("\"", "\\\"").replace("\n", " \\\n").replace("<br/>", "\n")
|
||||
rustTemplate(
|
||||
"""##[deprecated(note = "$messageEscaped")]""",
|
||||
"ProvideErrorMetadata" to RuntimeType.provideErrorMetadataTrait(rc),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -119,7 +119,10 @@ class ClientEnumGeneratorTest {
|
|||
"""
|
||||
assert_eq!(SomeEnum::from("Unknown"), SomeEnum::UnknownValue);
|
||||
assert_eq!(SomeEnum::from("UnknownValue"), SomeEnum::UnknownValue_);
|
||||
assert_eq!(SomeEnum::from("SomethingNew"), SomeEnum::Unknown(crate::primitives::UnknownVariantValue("SomethingNew".to_owned())));
|
||||
assert_eq!(
|
||||
SomeEnum::from("SomethingNew"),
|
||||
SomeEnum::Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue("SomethingNew".to_owned()))
|
||||
);
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
@ -150,7 +153,10 @@ class ClientEnumGeneratorTest {
|
|||
assert_eq!(instance.as_str(), "t2.micro");
|
||||
assert_eq!(InstanceType::from("t2.nano"), InstanceType::T2Nano);
|
||||
// round trip unknown variants:
|
||||
assert_eq!(InstanceType::from("other"), InstanceType::Unknown(crate::primitives::UnknownVariantValue("other".to_owned())));
|
||||
assert_eq!(
|
||||
InstanceType::from("other"),
|
||||
InstanceType::Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue("other".to_owned()))
|
||||
);
|
||||
assert_eq!(InstanceType::from("other").as_str(), "other");
|
||||
""",
|
||||
)
|
||||
|
|
|
@ -51,7 +51,7 @@ class ClientEventStreamUnmarshallerGeneratorTest {
|
|||
let result = $generator::new().unmarshall(&message);
|
||||
assert!(result.is_ok(), "expected ok, got: {:?}", result);
|
||||
match expect_error(result.unwrap()) {
|
||||
TestStreamError::Unhandled(err) => {
|
||||
err @ TestStreamError::Unhandled(_) => {
|
||||
let message = format!("{}", crate::error::DisplayErrorContext(&err));
|
||||
let expected = "message: \"unmodeled error\"";
|
||||
assert!(message.contains(expected), "Expected '{message}' to contain '{expected}'");
|
||||
|
|
|
@ -425,7 +425,6 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null)
|
|||
fun provideErrorMetadataTrait(runtimeConfig: RuntimeConfig) =
|
||||
smithyTypes(runtimeConfig).resolve("error::metadata::ProvideErrorMetadata")
|
||||
|
||||
fun unhandledError(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("error::Unhandled")
|
||||
fun jsonErrors(runtimeConfig: RuntimeConfig) = forInlineDependency(InlineDependency.jsonErrors(runtimeConfig))
|
||||
fun awsQueryCompatibleErrors(runtimeConfig: RuntimeConfig) =
|
||||
forInlineDependency(InlineDependency.awsQueryCompatibleErrors(runtimeConfig))
|
||||
|
|
|
@ -2,21 +2,20 @@
|
|||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
/// Copyright © 2023, Amazon, LLC.
|
||||
///
|
||||
/// This example demonstrates how to handle service generated errors.
|
||||
///
|
||||
/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734.
|
||||
/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md)
|
||||
/// file for instructions on how to launch the service locally.
|
||||
///
|
||||
/// The example can be run using `cargo run --example handling-errors`.
|
||||
///
|
||||
|
||||
//! This example demonstrates how to handle service generated errors.
|
||||
//!
|
||||
//! The example assumes that the Pokémon service is running on the localhost on TCP port 13734.
|
||||
//! Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md)
|
||||
//! file for instructions on how to launch the service locally.
|
||||
//!
|
||||
//! The example can be run using `cargo run --example handling-errors`.
|
||||
|
||||
use pokemon_service_client::error::DisplayErrorContext;
|
||||
use pokemon_service_client::Client as PokemonClient;
|
||||
use pokemon_service_client::{error::SdkError, operation::get_storage::GetStorageError};
|
||||
use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL};
|
||||
|
||||
use pokemon_service_client::Client as PokemonClient;
|
||||
|
||||
/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -77,14 +76,10 @@ async fn main() {
|
|||
GetStorageError::ValidationError(ve) => {
|
||||
tracing::error!(error = %ve, "A required field has not been set.");
|
||||
}
|
||||
// An unexpected error occurred (e.g., invalid JSON returned by the service or an unknown error code).
|
||||
GetStorageError::Unhandled(uh) => {
|
||||
tracing::error!(error = %uh, "An unhandled error has occurred.")
|
||||
}
|
||||
// The SdkError is marked as `#[non_exhaustive]`. Therefore, a catch-all pattern is required to handle
|
||||
// potential future variants introduced in SdkError.
|
||||
_ => {
|
||||
tracing::error!(error = %se.err(), "Some other error has occurred on the server")
|
||||
tracing::error!(error = %DisplayErrorContext(se.err()), "Some other error has occurred on the server")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,5 +8,6 @@ allowed_external_types = [
|
|||
"http::header::value::HeaderValue",
|
||||
"http::request::Request",
|
||||
"http::response::Response",
|
||||
"http::status::StatusCode",
|
||||
"http::uri::Uri",
|
||||
]
|
||||
|
|
|
@ -13,4 +13,4 @@ mod response;
|
|||
pub use error::HttpError;
|
||||
pub use headers::{HeaderValue, Headers, HeadersIter};
|
||||
pub use request::{Request, RequestParts};
|
||||
pub use response::Response;
|
||||
pub use response::{Response, StatusCode};
|
||||
|
|
|
@ -13,6 +13,8 @@ pub mod operation;
|
|||
mod unhandled;
|
||||
|
||||
pub use metadata::ErrorMetadata;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub use unhandled::Unhandled;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#![allow(deprecated)]
|
||||
|
||||
//! Unhandled error type.
|
||||
|
||||
use crate::error::{metadata::ProvideErrorMetadata, ErrorMetadata};
|
||||
use std::error::Error as StdError;
|
||||
|
||||
#[deprecated(note = "The `Unhandled` type is no longer used by errors.")]
|
||||
/// Builder for [`Unhandled`]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Builder {
|
||||
|
@ -58,6 +61,7 @@ impl Builder {
|
|||
/// [`DisplayErrorContext`](crate::error::display::DisplayErrorContext), use another
|
||||
/// error reporter library that visits the error's cause/source chain, or call
|
||||
/// [`Error::source`](std::error::Error::source) for more details about the underlying cause.
|
||||
#[deprecated(note = "This type is no longer used by errors.")]
|
||||
#[derive(Debug)]
|
||||
pub struct Unhandled {
|
||||
source: Box<dyn StdError + Send + Sync + 'static>,
|
||||
|
|
Loading…
Reference in New Issue