From cb64f4f7a7b10705dfd6688eaa3f4b2dbec64d6e Mon Sep 17 00:00:00 2001 From: Joe Wu Date: Fri, 14 Nov 2025 09:47:53 -0800 Subject: [PATCH 1/7] Add document discrinator sanitizer for JSON protocols --- .../aws-client-awsjson/build.gradle.kts | 1 + .../jsonprotocols/AwsJson11ProtocolTests.java | 47 +++++++++++++++++++ .../jsonprotocols/AwsJson1ProtocolTests.java | 2 - .../aws/client/awsjson/AwsJson11Protocol.java | 3 +- .../aws/client/awsjson/AwsJson1Protocol.java | 3 +- .../aws/client/awsjson/AwsJsonProtocol.java | 9 +++- .../aws/restjson/RestJson1ProtocolTests.java | 1 - .../restjson/RestJsonClientProtocol.java | 2 + .../smithy/java/json/ErrorTypeSanitizer.java | 37 +++++++++++++++ .../amazon/smithy/java/json/JsonCodec.java | 12 +++++ .../smithy/java/json/JsonDocuments.java | 3 ++ .../amazon/smithy/java/json/JsonSettings.java | 24 ++++++++++ 12 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson11ProtocolTests.java create mode 100644 codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java diff --git a/aws/client/aws-client-awsjson/build.gradle.kts b/aws/client/aws-client-awsjson/build.gradle.kts index 1e3af0971..3024526ca 100644 --- a/aws/client/aws-client-awsjson/build.gradle.kts +++ b/aws/client/aws-client-awsjson/build.gradle.kts @@ -20,3 +20,4 @@ dependencies { val generator = "software.amazon.smithy.java.protocoltests.generators.ProtocolTestGenerator" addGenerateSrcsTask(generator, "awsJson1_0", "aws.protocoltests.json10#JsonRpc10") +addGenerateSrcsTask(generator, "awsJson1_1", "aws.protocoltests.json#JsonProtocol") diff --git a/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson11ProtocolTests.java b/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson11ProtocolTests.java new file mode 100644 index 000000000..b62bdb1bd --- /dev/null +++ b/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson11ProtocolTests.java @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.client.aws.jsonprotocols; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; +import software.amazon.smithy.java.io.ByteBufferUtils; +import software.amazon.smithy.java.io.datastream.DataStream; +import software.amazon.smithy.java.protocoltests.harness.*; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; + +@ProtocolTest( + service = "aws.protocoltests.json#JsonProtocol", + testType = TestType.CLIENT) +public class AwsJson11ProtocolTests { + @HttpClientRequestTests + @ProtocolTestFilter( + skipTests = { + "SDKAppliedContentEncoding_awsJson1_1", + "SDKAppendsGzipAndIgnoresHttpProvidedEncoding_awsJson1_1", + }) + public void requestTest(DataStream expected, DataStream actual) { + Node expectedNode = ObjectNode.objectNode(); + if (expected.contentLength() != 0) { + expectedNode = Node.parse(new String(ByteBufferUtils.getBytes(expected.asByteBuffer()), + StandardCharsets.UTF_8)); + } + Node actualNode = Node.parse(new StringBuildingSubscriber(actual).getResult()); + assertEquals(expectedNode, actualNode); + } + + @HttpClientResponseTests + @ProtocolTestFilter( + skipTests = { + "AwsJson11FooErrorUsingCode", + "AwsJson11FooErrorUsingCodeAndNamespace", + "AwsJson11FooErrorUsingCodeUriAndNamespace", + }) + public void responseTest(Runnable test) { + test.run(); + } +} diff --git a/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson1ProtocolTests.java b/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson1ProtocolTests.java index 36d4cf867..7908efcf5 100644 --- a/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson1ProtocolTests.java +++ b/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson1ProtocolTests.java @@ -55,8 +55,6 @@ public void requestTest(DataStream expected, DataStream actual) { "AwsJson10FooErrorUsingCode", "AwsJson10FooErrorUsingCodeAndNamespace", "AwsJson10FooErrorUsingCodeUriAndNamespace", - "AwsJson10FooErrorWithDunderTypeUriAndNamespace" - }, skipOperations = "aws.protocoltests.json10#OperationWithRequiredMembersWithDefaults") public void responseTest(Runnable test) { diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java index d69a3a2a1..01e0ddf98 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java @@ -10,6 +10,7 @@ import software.amazon.smithy.java.client.core.ClientProtocol; import software.amazon.smithy.java.client.core.ClientProtocolFactory; import software.amazon.smithy.java.client.core.ProtocolSettings; +import software.amazon.smithy.java.json.ErrorTypeSanitizer; import software.amazon.smithy.model.shapes.ShapeId; /** @@ -24,7 +25,7 @@ public final class AwsJson11Protocol extends AwsJsonProtocol { * discriminator of documents that use relative shape IDs. */ public AwsJson11Protocol(ShapeId service) { - super(TRAIT_ID, service); + super(TRAIT_ID, service, ErrorTypeSanitizer::REMOVE_NAMESPACE_AND_URI); } @Override diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java index 1fa56271a..27508cabe 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java @@ -10,6 +10,7 @@ import software.amazon.smithy.java.client.core.ClientProtocol; import software.amazon.smithy.java.client.core.ClientProtocolFactory; import software.amazon.smithy.java.client.core.ProtocolSettings; +import software.amazon.smithy.java.json.ErrorTypeSanitizer; import software.amazon.smithy.model.shapes.ShapeId; /** @@ -24,7 +25,7 @@ public final class AwsJson1Protocol extends AwsJsonProtocol { * discriminator of documents that use relative shape IDs. */ public AwsJson1Protocol(ShapeId service) { - super(TRAIT_ID, service); + super(TRAIT_ID, service, ErrorTypeSanitizer::REMOVE_URI); } @Override diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJsonProtocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJsonProtocol.java index 05703ef5e..9b407ce41 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJsonProtocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJsonProtocol.java @@ -9,6 +9,7 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; +import java.util.function.Function; import software.amazon.smithy.java.client.http.AmznErrorHeaderExtractor; import software.amazon.smithy.java.client.http.HttpClientProtocol; import software.amazon.smithy.java.client.http.HttpErrorDeserializer; @@ -39,10 +40,14 @@ abstract sealed class AwsJsonProtocol extends HttpClientProtocol permits AwsJson * @param service The service ID used to make X-Amz-Target, and the namespace is used when finding the * discriminator of documents that use relative shape IDs. */ - public AwsJsonProtocol(ShapeId trait, ShapeId service) { + public AwsJsonProtocol(ShapeId trait, ShapeId service, Function errorTypeSanitizer) { super(trait); this.service = service; - this.codec = JsonCodec.builder().defaultNamespace(service.getNamespace()).build(); + this.codec = JsonCodec.builder() + .defaultNamespace(service.getNamespace()) + .errorTypeSanitizer(errorTypeSanitizer) + .useTimestampFormat(true) + .build(); this.errorDeserializer = HttpErrorDeserializer.builder() .codec(codec) diff --git a/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java b/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java index e5dc54b93..1fb6a91b8 100644 --- a/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java +++ b/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java @@ -61,7 +61,6 @@ public void requestTest(DataStream expected, DataStream actual) { "RestJsonFooErrorUsingCode", "RestJsonFooErrorUsingCodeAndNamespace", "RestJsonFooErrorUsingCodeUriAndNamespace", - "RestJsonFooErrorWithDunderTypeUriAndNamespace" }) public void responseTest(Runnable test) { test.run(); diff --git a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java index f3123c398..9c20c3e62 100644 --- a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java +++ b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java @@ -29,6 +29,7 @@ import software.amazon.smithy.java.core.serde.event.EventStreamingException; import software.amazon.smithy.java.http.api.HttpRequest; import software.amazon.smithy.java.http.binding.RequestSerializer; +import software.amazon.smithy.java.json.ErrorTypeSanitizer; import software.amazon.smithy.java.json.JsonCodec; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; @@ -52,6 +53,7 @@ public RestJsonClientProtocol(ShapeId service) { .useJsonName(true) .useTimestampFormat(true) .defaultNamespace(service.getNamespace()) + .errorTypeSanitizer(ErrorTypeSanitizer::REMOVE_NAMESPACE_AND_URI) .build(); this.errorDeserializer = HttpErrorDeserializer.builder() diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java new file mode 100644 index 000000000..f3564b31c --- /dev/null +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.json; + +/** + * A util class contains the sanitizer functions to process {@code __type} field from the document + * + *

For example, given {@code __type = "aws.protocoltests.restjson#FooError:http://abc.com"}, + * different protocols have different requirements for the {@code __type} field. This class provides + * sanitizers to remove trailing URIs or leading namespaces from {@code __type}. + */ +public class ErrorTypeSanitizer { + public static String REMOVE_URI(String text) { + if (text == null) { + return null; + } + var colon = text.indexOf(':'); + if (colon > 0) { + text = text.substring(0, colon); + } + return text; + } + + public static String REMOVE_NAMESPACE_AND_URI(String text) { + if (text == null) { + return null; + } + var hash = text.indexOf('#'); + if (hash > 0) { + text = text.substring(hash + 1); + } + return REMOVE_URI(text); + } +} diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonCodec.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonCodec.java index 3c61450bd..5070b0b46 100644 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonCodec.java +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonCodec.java @@ -7,6 +7,7 @@ import java.io.OutputStream; import java.nio.ByteBuffer; +import java.util.function.Function; import software.amazon.smithy.java.core.serde.Codec; import software.amazon.smithy.java.core.serde.ShapeDeserializer; import software.amazon.smithy.java.core.serde.ShapeSerializer; @@ -174,5 +175,16 @@ Builder overrideSerdeProvider(JsonSerdeProvider provider) { settingsBuilder.overrideSerdeProvider(provider); return this; } + + /** + * Uses a custom error type sanitizer to process the {@code __type} field. + * + * @param errorTypeSanitizer the type sanitizer to use. + * @return the builder. + */ + public Builder errorTypeSanitizer(Function errorTypeSanitizer) { + settingsBuilder.errorTypeSanitizer(errorTypeSanitizer); + return this; + } } } diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java index d019456cb..d2e0142cd 100644 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java @@ -250,6 +250,9 @@ public ShapeId discriminator() { if (member != null && member.type() == ShapeType.STRING) { discriminator = member.asString(); } + if (settings.errorTypeSanitizer() != null) { + discriminator = settings.errorTypeSanitizer().apply(discriminator); + } return DocumentDeserializer.parseDiscriminator(discriminator, settings.defaultNamespace()); } diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java index 437dbd1ea..7ffe08f52 100644 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java @@ -7,6 +7,7 @@ import java.util.Objects; import java.util.ServiceLoader; +import java.util.function.Function; import software.amazon.smithy.java.core.serde.TimestampFormatter; /** @@ -45,6 +46,7 @@ public final class JsonSettings { private final JsonSerdeProvider provider; private final boolean serializeTypeInDocuments; private final boolean prettyPrint; + private final Function errorTypeSanitizer; private JsonSettings(Builder builder) { this.timestampResolver = builder.useTimestampFormat @@ -58,6 +60,7 @@ private JsonSettings(Builder builder) { this.provider = builder.provider; this.serializeTypeInDocuments = builder.serializeTypeInDocuments; this.prettyPrint = builder.prettyPrint; + this.errorTypeSanitizer = builder.errorTypeSanitizer; } /** @@ -115,6 +118,15 @@ public boolean serializeTypeInDocuments() { return serializeTypeInDocuments; } + /** + * The error type sanitizer to use for {@code __type}. Default is null + * + * @return the sanitizer used or null + */ + public Function errorTypeSanitizer() { + return errorTypeSanitizer; + } + /** * Whether to format the JSON output with pretty printing (indentation and line breaks). * @@ -165,6 +177,7 @@ public static final class Builder { private JsonSerdeProvider provider = PROVIDER; private boolean serializeTypeInDocuments = true; private boolean prettyPrint = false; + private Function errorTypeSanitizer; private Builder() {} @@ -275,5 +288,16 @@ Builder overrideSerdeProvider(JsonSerdeProvider provider) { this.provider = Objects.requireNonNull(provider); return this; } + + /** + * Uses a custom error type sanitizer for error type + * + * @param errorTypeSanitizer the sanitizer to use for error type. + * @return the builder. + */ + Builder errorTypeSanitizer(Function errorTypeSanitizer) { + this.errorTypeSanitizer = errorTypeSanitizer; + return this; + } } } From 5b95834be2b164dd4a4f8856cb3012e84d4dfcac Mon Sep 17 00:00:00 2001 From: Joe Wu Date: Wed, 19 Nov 2025 13:45:04 -0800 Subject: [PATCH 2/7] Address comments --- .../aws/client/awsjson/AwsJson11Protocol.java | 2 +- .../aws/client/awsjson/AwsJson1Protocol.java | 2 +- .../restjson/RestJsonClientProtocol.java | 2 +- .../smithy/java/json/ErrorTypeSanitizer.java | 6 ++-- .../amazon/smithy/java/json/JsonSettings.java | 2 +- .../java/json/ErrorTypeSanitizerTest.java | 32 +++++++++++++++++++ 6 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java index 01e0ddf98..993e0f5c0 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java @@ -25,7 +25,7 @@ public final class AwsJson11Protocol extends AwsJsonProtocol { * discriminator of documents that use relative shape IDs. */ public AwsJson11Protocol(ShapeId service) { - super(TRAIT_ID, service, ErrorTypeSanitizer::REMOVE_NAMESPACE_AND_URI); + super(TRAIT_ID, service, ErrorTypeSanitizer::removeNamespaceAndUri); } @Override diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java index 27508cabe..75244d51e 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java @@ -25,7 +25,7 @@ public final class AwsJson1Protocol extends AwsJsonProtocol { * discriminator of documents that use relative shape IDs. */ public AwsJson1Protocol(ShapeId service) { - super(TRAIT_ID, service, ErrorTypeSanitizer::REMOVE_URI); + super(TRAIT_ID, service, ErrorTypeSanitizer::removeUri); } @Override diff --git a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java index 9c20c3e62..55009f384 100644 --- a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java +++ b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java @@ -53,7 +53,7 @@ public RestJsonClientProtocol(ShapeId service) { .useJsonName(true) .useTimestampFormat(true) .defaultNamespace(service.getNamespace()) - .errorTypeSanitizer(ErrorTypeSanitizer::REMOVE_NAMESPACE_AND_URI) + .errorTypeSanitizer(ErrorTypeSanitizer::removeNamespaceAndUri) .build(); this.errorDeserializer = HttpErrorDeserializer.builder() diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java index f3564b31c..a2ed56ffb 100644 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java @@ -13,7 +13,7 @@ * sanitizers to remove trailing URIs or leading namespaces from {@code __type}. */ public class ErrorTypeSanitizer { - public static String REMOVE_URI(String text) { + public static String removeUri(String text) { if (text == null) { return null; } @@ -24,7 +24,7 @@ public static String REMOVE_URI(String text) { return text; } - public static String REMOVE_NAMESPACE_AND_URI(String text) { + public static String removeNamespaceAndUri(String text) { if (text == null) { return null; } @@ -32,6 +32,6 @@ public static String REMOVE_NAMESPACE_AND_URI(String text) { if (hash > 0) { text = text.substring(hash + 1); } - return REMOVE_URI(text); + return removeUri(text); } } diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java index 7ffe08f52..b4370ecd9 100644 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java @@ -295,7 +295,7 @@ Builder overrideSerdeProvider(JsonSerdeProvider provider) { * @param errorTypeSanitizer the sanitizer to use for error type. * @return the builder. */ - Builder errorTypeSanitizer(Function errorTypeSanitizer) { + public Builder errorTypeSanitizer(Function errorTypeSanitizer) { this.errorTypeSanitizer = errorTypeSanitizer; return this; } diff --git a/codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java b/codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java new file mode 100644 index 000000000..163ef3dd6 --- /dev/null +++ b/codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java @@ -0,0 +1,32 @@ +package software.amazon.smithy.java.json; + +import java.util.List; +import org.junit.jupiter.api.Test; + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ErrorTypeSanitizerTest { + private static final String SIMPLE_TYPE = "FooError"; + private static final String TYPE_WITH_URI = "FooError:http://amazon.com/smithy/com.amazon.smithy.validate/"; + private static final String TYPE_WITH_NAMESPACE = "aws.protocoltests.restjson#FooError"; + private static final String TYPE_WITH_NAMESPACE_AND_URI = "aws.protocoltests.restjson#FooError:http://amazon.com/smithy/com.amazon.smithy.validate/"; + @Test + public void testRemoveUri() { + String expected = "FooError"; + String expectedWithNamespace = "aws.protocoltests.restjson#FooError"; + assertEquals(expected, ErrorTypeSanitizer.removeUri(SIMPLE_TYPE)); + assertEquals(expected, ErrorTypeSanitizer.removeUri(TYPE_WITH_URI)); + assertEquals(expectedWithNamespace, ErrorTypeSanitizer.removeUri(TYPE_WITH_NAMESPACE)); + assertEquals(expectedWithNamespace, ErrorTypeSanitizer.removeUri(TYPE_WITH_NAMESPACE_AND_URI)); + } + + @Test + public void testRemoveNameSpaceAndUri() { + String expected = "FooError"; + assertEquals(expected, ErrorTypeSanitizer.removeNamespaceAndUri(SIMPLE_TYPE)); + assertEquals(expected, ErrorTypeSanitizer.removeNamespaceAndUri(TYPE_WITH_URI)); + assertEquals(expected, ErrorTypeSanitizer.removeNamespaceAndUri(TYPE_WITH_NAMESPACE)); + assertEquals(expected, ErrorTypeSanitizer.removeNamespaceAndUri(TYPE_WITH_NAMESPACE_AND_URI)); + } +} From 6539ddadee820ea836f593e9029eafbee1895ef0 Mon Sep 17 00:00:00 2001 From: Joe Wu Date: Wed, 19 Nov 2025 13:50:30 -0800 Subject: [PATCH 3/7] Apply spotless --- .../smithy/java/json/ErrorTypeSanitizerTest.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java b/codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java index 163ef3dd6..d8f5e3ce3 100644 --- a/codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java +++ b/codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java @@ -1,16 +1,20 @@ -package software.amazon.smithy.java.json; - -import java.util.List; -import org.junit.jupiter.api.Test; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.java.json; import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + public class ErrorTypeSanitizerTest { private static final String SIMPLE_TYPE = "FooError"; private static final String TYPE_WITH_URI = "FooError:http://amazon.com/smithy/com.amazon.smithy.validate/"; private static final String TYPE_WITH_NAMESPACE = "aws.protocoltests.restjson#FooError"; - private static final String TYPE_WITH_NAMESPACE_AND_URI = "aws.protocoltests.restjson#FooError:http://amazon.com/smithy/com.amazon.smithy.validate/"; + private static final String TYPE_WITH_NAMESPACE_AND_URI = + "aws.protocoltests.restjson#FooError:http://amazon.com/smithy/com.amazon.smithy.validate/"; @Test public void testRemoveUri() { String expected = "FooError"; From 8e9c935f05e5b70075ddea3a5e862fc59592723f Mon Sep 17 00:00:00 2001 From: Joe Wu Date: Mon, 24 Nov 2025 15:44:22 -0800 Subject: [PATCH 4/7] Add handling for code based exceptions --- .../aws/jsonprotocols/AwsJson11ProtocolTests.java | 6 ------ .../aws/jsonprotocols/AwsJson1ProtocolTests.java | 4 ---- .../client/aws/restjson/RestJson1ProtocolTests.java | 6 ------ .../amazon/smithy/java/json/JsonDocuments.java | 11 ++++++++--- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson11ProtocolTests.java b/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson11ProtocolTests.java index b62bdb1bd..5183f3960 100644 --- a/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson11ProtocolTests.java +++ b/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson11ProtocolTests.java @@ -35,12 +35,6 @@ public void requestTest(DataStream expected, DataStream actual) { } @HttpClientResponseTests - @ProtocolTestFilter( - skipTests = { - "AwsJson11FooErrorUsingCode", - "AwsJson11FooErrorUsingCodeAndNamespace", - "AwsJson11FooErrorUsingCodeUriAndNamespace", - }) public void responseTest(Runnable test) { test.run(); } diff --git a/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson1ProtocolTests.java b/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson1ProtocolTests.java index 7908efcf5..4f2885241 100644 --- a/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson1ProtocolTests.java +++ b/aws/client/aws-client-awsjson/src/it/java/software/amazon/smithy/java/client/aws/jsonprotocols/AwsJson1ProtocolTests.java @@ -51,10 +51,6 @@ public void requestTest(DataStream expected, DataStream actual) { skipTests = { "AwsJson10ClientPopulatesDefaultsValuesWhenMissingInResponse", "AwsJson10ClientIgnoresDefaultValuesIfMemberValuesArePresentInResponse", - //The below fail because we haven't implemented code based exception handling - "AwsJson10FooErrorUsingCode", - "AwsJson10FooErrorUsingCodeAndNamespace", - "AwsJson10FooErrorUsingCodeUriAndNamespace", }, skipOperations = "aws.protocoltests.json10#OperationWithRequiredMembersWithDefaults") public void responseTest(Runnable test) { diff --git a/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java b/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java index 1fb6a91b8..f41c1c1e8 100644 --- a/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java +++ b/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java @@ -56,12 +56,6 @@ public void requestTest(DataStream expected, DataStream actual) { } @HttpClientResponseTests - @ProtocolTestFilter( - skipTests = { - "RestJsonFooErrorUsingCode", - "RestJsonFooErrorUsingCodeAndNamespace", - "RestJsonFooErrorUsingCodeUriAndNamespace", - }) public void responseTest(Runnable test) { test.run(); } diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java index d2e0142cd..d49824614 100644 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java @@ -246,9 +246,14 @@ public ShapeType type() { @Override public ShapeId discriminator() { String discriminator = null; - var member = values.get("__type"); - if (member != null && member.type() == ShapeType.STRING) { - discriminator = member.asString(); + var typeMember = values.get("__type"); + if (typeMember != null && typeMember.type() == ShapeType.STRING) { + discriminator = typeMember.asString(); + } else { + var codeMember = values.get("code"); + if (codeMember != null && codeMember.type() == ShapeType.STRING) { + discriminator = codeMember.asString(); + } } if (settings.errorTypeSanitizer() != null) { discriminator = settings.errorTypeSanitizer().apply(discriminator); From 307386a8f80040843f45ebca2be50c294e531ba5 Mon Sep 17 00:00:00 2001 From: Joe Wu Date: Wed, 3 Dec 2025 14:18:09 -0800 Subject: [PATCH 5/7] Add new method to parse errortype from document, move error sanitizer to documentutils and add sanitizer for cbor --- .../aws/client/awsjson/AwsJson11Protocol.java | 4 +- .../aws/client/awsjson/AwsJson1Protocol.java | 4 +- .../restjson/RestJsonClientProtocol.java | 4 +- .../client/http/HttpErrorDeserializer.java | 2 +- .../smithy/java/cbor/CborDocuments.java | 21 +++++++--- .../smithy/java/json/ErrorTypeSanitizer.java | 37 ----------------- .../smithy/java/json/JsonDocuments.java | 18 +++++++-- .../amazon/smithy/java/json/JsonSettings.java | 3 +- .../java/core/serde/document/Document.java | 12 ++++++ .../core/serde/document/DocumentUtils.java | 40 +++++++++++++++++++ .../serde/document/DocumentUtilsTest.java | 21 +++++----- 11 files changed, 102 insertions(+), 64 deletions(-) delete mode 100644 codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java rename codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java => core/src/test/java/software/amazon/smithy/java/core/serde/document/DocumentUtilsTest.java (53%) diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java index 993e0f5c0..43155fb58 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java @@ -10,7 +10,7 @@ import software.amazon.smithy.java.client.core.ClientProtocol; import software.amazon.smithy.java.client.core.ClientProtocolFactory; import software.amazon.smithy.java.client.core.ProtocolSettings; -import software.amazon.smithy.java.json.ErrorTypeSanitizer; +import software.amazon.smithy.java.core.serde.document.DocumentUtils; import software.amazon.smithy.model.shapes.ShapeId; /** @@ -25,7 +25,7 @@ public final class AwsJson11Protocol extends AwsJsonProtocol { * discriminator of documents that use relative shape IDs. */ public AwsJson11Protocol(ShapeId service) { - super(TRAIT_ID, service, ErrorTypeSanitizer::removeNamespaceAndUri); + super(TRAIT_ID, service, DocumentUtils::removeNamespaceAndUri); } @Override diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java index 75244d51e..e7bbe114c 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java @@ -10,7 +10,7 @@ import software.amazon.smithy.java.client.core.ClientProtocol; import software.amazon.smithy.java.client.core.ClientProtocolFactory; import software.amazon.smithy.java.client.core.ProtocolSettings; -import software.amazon.smithy.java.json.ErrorTypeSanitizer; +import software.amazon.smithy.java.core.serde.document.DocumentUtils; import software.amazon.smithy.model.shapes.ShapeId; /** @@ -25,7 +25,7 @@ public final class AwsJson1Protocol extends AwsJsonProtocol { * discriminator of documents that use relative shape IDs. */ public AwsJson1Protocol(ShapeId service) { - super(TRAIT_ID, service, ErrorTypeSanitizer::removeUri); + super(TRAIT_ID, service, DocumentUtils::removeUri); } @Override diff --git a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java index 55009f384..7a84c329e 100644 --- a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java +++ b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java @@ -24,12 +24,12 @@ import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.schema.TraitKey; import software.amazon.smithy.java.core.serde.Codec; +import software.amazon.smithy.java.core.serde.document.DocumentUtils; import software.amazon.smithy.java.core.serde.event.EventDecoderFactory; import software.amazon.smithy.java.core.serde.event.EventEncoderFactory; import software.amazon.smithy.java.core.serde.event.EventStreamingException; import software.amazon.smithy.java.http.api.HttpRequest; import software.amazon.smithy.java.http.binding.RequestSerializer; -import software.amazon.smithy.java.json.ErrorTypeSanitizer; import software.amazon.smithy.java.json.JsonCodec; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; @@ -53,7 +53,7 @@ public RestJsonClientProtocol(ShapeId service) { .useJsonName(true) .useTimestampFormat(true) .defaultNamespace(service.getNamespace()) - .errorTypeSanitizer(ErrorTypeSanitizer::removeNamespaceAndUri) + .errorTypeSanitizer(DocumentUtils::removeNamespaceAndUri) .build(); this.errorDeserializer = HttpErrorDeserializer.builder() diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java index cb276721a..8ccbe46d4 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java @@ -202,7 +202,7 @@ public ModeledException createErrorFromDocument( HttpResponse response, ByteBuffer buffer) -> { var document = codec.createDeserializer(buffer).readDocument(); - var id = document.discriminator(); + var id = document.parseErrorType(); var builder = typeRegistry.createBuilder(id, ModeledException.class); if (builder != null) { return knownErrorFactory.createErrorFromDocument( diff --git a/codecs/cbor-codec/src/main/java/software/amazon/smithy/java/cbor/CborDocuments.java b/codecs/cbor-codec/src/main/java/software/amazon/smithy/java/cbor/CborDocuments.java index 0344e8e32..564cb7551 100644 --- a/codecs/cbor-codec/src/main/java/software/amazon/smithy/java/cbor/CborDocuments.java +++ b/codecs/cbor-codec/src/main/java/software/amazon/smithy/java/cbor/CborDocuments.java @@ -13,6 +13,7 @@ import software.amazon.smithy.java.core.serde.ShapeSerializer; import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.java.core.serde.document.DocumentDeserializer; +import software.amazon.smithy.java.core.serde.document.DocumentUtils; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.utils.SmithyInternalApi; @@ -51,11 +52,13 @@ public ShapeType type() { @Override public ShapeId discriminator() { - String discriminator = null; - var member = values.get("__type"); - if (member != null && member.type() == ShapeType.STRING) { - discriminator = member.asString(); - } + var discriminator = extractType(); + return DocumentDeserializer.parseDiscriminator(discriminator, settings.defaultNamespace()); + } + + @Override + public ShapeId parseErrorType() { + var discriminator = DocumentUtils.removeUri(extractType()); return DocumentDeserializer.parseDiscriminator(discriminator, settings.defaultNamespace()); } @@ -102,6 +105,14 @@ public boolean equals(Object obj) { public int hashCode() { return values.hashCode(); } + + private String extractType() { + var member = values.get("__type"); + if (member != null && member.type() == ShapeType.STRING) { + return member.asString(); + } + return null; + } } /** diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java deleted file mode 100644 index a2ed56ffb..000000000 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/ErrorTypeSanitizer.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.java.json; - -/** - * A util class contains the sanitizer functions to process {@code __type} field from the document - * - *

For example, given {@code __type = "aws.protocoltests.restjson#FooError:http://abc.com"}, - * different protocols have different requirements for the {@code __type} field. This class provides - * sanitizers to remove trailing URIs or leading namespaces from {@code __type}. - */ -public class ErrorTypeSanitizer { - public static String removeUri(String text) { - if (text == null) { - return null; - } - var colon = text.indexOf(':'); - if (colon > 0) { - text = text.substring(0, colon); - } - return text; - } - - public static String removeNamespaceAndUri(String text) { - if (text == null) { - return null; - } - var hash = text.indexOf('#'); - if (hash > 0) { - text = text.substring(hash + 1); - } - return removeUri(text); - } -} diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java index d49824614..f187589a2 100644 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java @@ -246,19 +246,29 @@ public ShapeType type() { @Override public ShapeId discriminator() { String discriminator = null; + var member = values.get("__type"); + if (member != null && member.type() == ShapeType.STRING) { + discriminator = member.asString(); + } + return DocumentDeserializer.parseDiscriminator(discriminator, settings.defaultNamespace()); + } + + @Override + public ShapeId parseErrorType() { + String errorType = null; var typeMember = values.get("__type"); if (typeMember != null && typeMember.type() == ShapeType.STRING) { - discriminator = typeMember.asString(); + errorType = typeMember.asString(); } else { var codeMember = values.get("code"); if (codeMember != null && codeMember.type() == ShapeType.STRING) { - discriminator = codeMember.asString(); + errorType = codeMember.asString(); } } if (settings.errorTypeSanitizer() != null) { - discriminator = settings.errorTypeSanitizer().apply(discriminator); + errorType = settings.errorTypeSanitizer().apply(errorType); } - return DocumentDeserializer.parseDiscriminator(discriminator, settings.defaultNamespace()); + return DocumentDeserializer.parseDiscriminator(errorType, settings.defaultNamespace()); } @Override diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java index b4370ecd9..fb6beeb26 100644 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java @@ -119,7 +119,8 @@ public boolean serializeTypeInDocuments() { } /** - * The error type sanitizer to use for {@code __type}. Default is null + * The error type sanitizer to use for {@code __type} or {@code code} when parsing the error discriminator. + * Default is null * * @return the sanitizer used or null */ diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java b/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java index 2f4dd45f0..5d6986365 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java @@ -125,6 +125,18 @@ default ShapeId discriminator() { return null; } + /** + * Attempts to find and parse a sanitized shape ID from the document in the document's discriminator field. + * + *

The discriminator of an error can be represented by either the __type field or the code field + * based on the protocol. + * + * @return the parsed shape ID, or null if not found. + */ + default ShapeId parseErrorType() { + return null; + } + /** * Serializes the Document as a document value in the Smithy data model. * diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/document/DocumentUtils.java b/core/src/main/java/software/amazon/smithy/java/core/serde/document/DocumentUtils.java index 7e8730f19..625057ca9 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/document/DocumentUtils.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/document/DocumentUtils.java @@ -175,4 +175,44 @@ public static T getMemberValue(Document container, Schema containerSchema, S .id() + "`: " + e.getMessage()); } } + + /** + * Removes the trailing URI in {@code __type} field or {@code code} field of the document + * + *

For example, given {@code __type = "aws.protocoltests.restjson#FooError:http://abc.com"}, + * protocols like restJSON should ignore the trailing URI and keep the namespace of the error type. + * + * @param text The error type string. + * @return The error type string without the trailing URI. + */ + public static String removeUri(String text) { + if (text == null) { + return null; + } + var colon = text.indexOf(':'); + if (colon > 0) { + text = text.substring(0, colon); + } + return text; + } + + /** + * Removes the namespace and trailing URI in {@code __type} field or {@code code} field of the document + * + *

For example, given {@code __type = "aws.protocoltests.restjson#FooError:http://abc.com"}, + * protocols like awsJSON 1.1 should ignore the namespace and the trailing URI of the error type. + * + * @param text The error type string. + * @return The error type string without the trailing URI. + */ + public static String removeNamespaceAndUri(String text) { + if (text == null) { + return null; + } + var hash = text.indexOf('#'); + if (hash > 0) { + text = text.substring(hash + 1); + } + return removeUri(text); + } } diff --git a/codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java b/core/src/test/java/software/amazon/smithy/java/core/serde/document/DocumentUtilsTest.java similarity index 53% rename from codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java rename to core/src/test/java/software/amazon/smithy/java/core/serde/document/DocumentUtilsTest.java index d8f5e3ce3..2f30cd1ac 100644 --- a/codecs/json-codec/src/test/java/software/amazon/smithy/java/json/ErrorTypeSanitizerTest.java +++ b/core/src/test/java/software/amazon/smithy/java/core/serde/document/DocumentUtilsTest.java @@ -3,34 +3,35 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.java.json; +package software.amazon.smithy.java.core.serde.document; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; -public class ErrorTypeSanitizerTest { +public class DocumentUtilsTest { private static final String SIMPLE_TYPE = "FooError"; private static final String TYPE_WITH_URI = "FooError:http://amazon.com/smithy/com.amazon.smithy.validate/"; private static final String TYPE_WITH_NAMESPACE = "aws.protocoltests.restjson#FooError"; private static final String TYPE_WITH_NAMESPACE_AND_URI = "aws.protocoltests.restjson#FooError:http://amazon.com/smithy/com.amazon.smithy.validate/"; + @Test public void testRemoveUri() { String expected = "FooError"; String expectedWithNamespace = "aws.protocoltests.restjson#FooError"; - assertEquals(expected, ErrorTypeSanitizer.removeUri(SIMPLE_TYPE)); - assertEquals(expected, ErrorTypeSanitizer.removeUri(TYPE_WITH_URI)); - assertEquals(expectedWithNamespace, ErrorTypeSanitizer.removeUri(TYPE_WITH_NAMESPACE)); - assertEquals(expectedWithNamespace, ErrorTypeSanitizer.removeUri(TYPE_WITH_NAMESPACE_AND_URI)); + assertEquals(expected, DocumentUtils.removeUri(SIMPLE_TYPE)); + assertEquals(expected, DocumentUtils.removeUri(TYPE_WITH_URI)); + assertEquals(expectedWithNamespace, DocumentUtils.removeUri(TYPE_WITH_NAMESPACE)); + assertEquals(expectedWithNamespace, DocumentUtils.removeUri(TYPE_WITH_NAMESPACE_AND_URI)); } @Test public void testRemoveNameSpaceAndUri() { String expected = "FooError"; - assertEquals(expected, ErrorTypeSanitizer.removeNamespaceAndUri(SIMPLE_TYPE)); - assertEquals(expected, ErrorTypeSanitizer.removeNamespaceAndUri(TYPE_WITH_URI)); - assertEquals(expected, ErrorTypeSanitizer.removeNamespaceAndUri(TYPE_WITH_NAMESPACE)); - assertEquals(expected, ErrorTypeSanitizer.removeNamespaceAndUri(TYPE_WITH_NAMESPACE_AND_URI)); + assertEquals(expected, DocumentUtils.removeNamespaceAndUri(SIMPLE_TYPE)); + assertEquals(expected, DocumentUtils.removeNamespaceAndUri(TYPE_WITH_URI)); + assertEquals(expected, DocumentUtils.removeNamespaceAndUri(TYPE_WITH_NAMESPACE)); + assertEquals(expected, DocumentUtils.removeNamespaceAndUri(TYPE_WITH_NAMESPACE_AND_URI)); } } From 8e9b84f37d7e9f1f91d65ad34d49237892089aff Mon Sep 17 00:00:00 2001 From: Joe Wu Date: Thu, 4 Dec 2025 14:07:56 -0800 Subject: [PATCH 6/7] Move the discriminator extraction for error to httpErrorDeserializer --- .../aws/client/awsjson/AwsJson11Protocol.java | 4 +- .../aws/client/awsjson/AwsJson1Protocol.java | 16 +++- .../aws/client/awsjson/AwsJsonProtocol.java | 9 +- .../restjson/RestJsonClientProtocol.java | 2 - .../java/client/http/ErrorTypeUtils.java | 84 +++++++++++++++++++ .../client/http/HttpErrorDeserializer.java | 63 ++++++++------ .../java/client/http/ErrorTypeUtilsTest.java | 56 +++++++++++++ .../java/client/rpcv2/RpcV2CborProtocol.java | 18 +++- .../smithy/java/cbor/CborDocuments.java | 21 ++--- .../smithy/java/json/JsonDocuments.java | 18 ---- .../java/core/serde/document/Document.java | 12 --- .../core/serde/document/DocumentUtils.java | 40 --------- .../serde/document/DocumentUtilsTest.java | 37 -------- 13 files changed, 223 insertions(+), 157 deletions(-) create mode 100644 client/client-http/src/main/java/software/amazon/smithy/java/client/http/ErrorTypeUtils.java create mode 100644 client/client-http/src/test/java/software/amazon/smithy/java/client/http/ErrorTypeUtilsTest.java delete mode 100644 core/src/test/java/software/amazon/smithy/java/core/serde/document/DocumentUtilsTest.java diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java index 43155fb58..ba8d654dc 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java @@ -10,7 +10,7 @@ import software.amazon.smithy.java.client.core.ClientProtocol; import software.amazon.smithy.java.client.core.ClientProtocolFactory; import software.amazon.smithy.java.client.core.ProtocolSettings; -import software.amazon.smithy.java.core.serde.document.DocumentUtils; +import software.amazon.smithy.java.client.http.HttpErrorDeserializer; import software.amazon.smithy.model.shapes.ShapeId; /** @@ -25,7 +25,7 @@ public final class AwsJson11Protocol extends AwsJsonProtocol { * discriminator of documents that use relative shape IDs. */ public AwsJson11Protocol(ShapeId service) { - super(TRAIT_ID, service, DocumentUtils::removeNamespaceAndUri); + super(TRAIT_ID, service, new HttpErrorDeserializer.DocumentPayloadParser()); } @Override diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java index e7bbe114c..5e781a617 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java @@ -10,7 +10,10 @@ import software.amazon.smithy.java.client.core.ClientProtocol; import software.amazon.smithy.java.client.core.ClientProtocolFactory; import software.amazon.smithy.java.client.core.ProtocolSettings; -import software.amazon.smithy.java.core.serde.document.DocumentUtils; +import software.amazon.smithy.java.client.http.ErrorTypeUtils; +import software.amazon.smithy.java.client.http.HttpErrorDeserializer; +import software.amazon.smithy.java.core.serde.document.Document; +import software.amazon.smithy.java.core.serde.document.DocumentDeserializer; import software.amazon.smithy.model.shapes.ShapeId; /** @@ -25,7 +28,16 @@ public final class AwsJson1Protocol extends AwsJsonProtocol { * discriminator of documents that use relative shape IDs. */ public AwsJson1Protocol(ShapeId service) { - super(TRAIT_ID, service, DocumentUtils::removeUri); + super(TRAIT_ID, service, new Json10Parser()); + } + + private static final class Json10Parser extends HttpErrorDeserializer.DocumentPayloadParser { + @Override + public ShapeId extractErrorType(Document document, String namespace) { + return DocumentDeserializer.parseDiscriminator( + ErrorTypeUtils.removeUri(ErrorTypeUtils.readTypeAndCode(document)), + namespace); + } } @Override diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJsonProtocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJsonProtocol.java index 9b407ce41..b76b42c3b 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJsonProtocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJsonProtocol.java @@ -9,7 +9,6 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; -import java.util.function.Function; import software.amazon.smithy.java.client.http.AmznErrorHeaderExtractor; import software.amazon.smithy.java.client.http.HttpClientProtocol; import software.amazon.smithy.java.client.http.HttpErrorDeserializer; @@ -40,18 +39,22 @@ abstract sealed class AwsJsonProtocol extends HttpClientProtocol permits AwsJson * @param service The service ID used to make X-Amz-Target, and the namespace is used when finding the * discriminator of documents that use relative shape IDs. */ - public AwsJsonProtocol(ShapeId trait, ShapeId service, Function errorTypeSanitizer) { + public AwsJsonProtocol( + ShapeId trait, + ShapeId service, + HttpErrorDeserializer.ErrorPayloadParser errorPayloadParser + ) { super(trait); this.service = service; this.codec = JsonCodec.builder() .defaultNamespace(service.getNamespace()) - .errorTypeSanitizer(errorTypeSanitizer) .useTimestampFormat(true) .build(); this.errorDeserializer = HttpErrorDeserializer.builder() .codec(codec) .serviceId(service) + .errorPayloadParser(errorPayloadParser) .headerErrorExtractor(new AmznErrorHeaderExtractor()) .build(); } diff --git a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java index 7a84c329e..f3123c398 100644 --- a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java +++ b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java @@ -24,7 +24,6 @@ import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.schema.TraitKey; import software.amazon.smithy.java.core.serde.Codec; -import software.amazon.smithy.java.core.serde.document.DocumentUtils; import software.amazon.smithy.java.core.serde.event.EventDecoderFactory; import software.amazon.smithy.java.core.serde.event.EventEncoderFactory; import software.amazon.smithy.java.core.serde.event.EventStreamingException; @@ -53,7 +52,6 @@ public RestJsonClientProtocol(ShapeId service) { .useJsonName(true) .useTimestampFormat(true) .defaultNamespace(service.getNamespace()) - .errorTypeSanitizer(DocumentUtils::removeNamespaceAndUri) .build(); this.errorDeserializer = HttpErrorDeserializer.builder() diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/ErrorTypeUtils.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/ErrorTypeUtils.java new file mode 100644 index 000000000..d67681c37 --- /dev/null +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/ErrorTypeUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.client.http; + +import software.amazon.smithy.java.core.serde.document.Document; +import software.amazon.smithy.model.shapes.ShapeType; + +public class ErrorTypeUtils { + + /** + * Read the error type from __type field of the document. + * + * @param document the document of the payload to read. + * @return the extracted error type from the document. + */ + public static String readType(Document document) { + String errorType = null; + var member = document.getMember("__type"); + if (member != null && member.type() == ShapeType.STRING) { + errorType = member.asString(); + } + return errorType; + } + + /** + * Read the error type from __type or code field of the document. + * + * @param document the document of the payload to read. + * @return the extracted error type from the document. + */ + public static String readTypeAndCode(Document document) { + String errorType = readType(document); + if (errorType == null) { + var member = document.getMember("code"); + if (member != null && member.type() == ShapeType.STRING) { + errorType = member.asString(); + } + } + return errorType; + } + + /** + * Removes the trailing URI in {@code __type} field or {@code code} field of the document. + * + *

For example, given {@code __type = "aws.protocoltests.restjson#FooError:http://abc.com"}, + * protocols like restJSON should ignore the trailing URI and keep the namespace of the error type. + * + * @param text The error type string. + * @return The error type string without the trailing URI. + */ + public static String removeUri(String text) { + if (text == null) { + return null; + } + var colon = text.indexOf(':'); + if (colon > 0) { + text = text.substring(0, colon); + } + return text; + } + + /** + * Removes the namespace and trailing URI in {@code __type} field or {@code code} field of the document. + * + *

For example, given {@code __type = "aws.protocoltests.restjson#FooError:http://abc.com"}, + * protocols like awsJSON 1.1 should ignore the namespace and the trailing URI of the error type. + * + * @param text The error type string. + * @return The error type string without the trailing URI. + */ + public static String removeNamespaceAndUri(String text) { + if (text == null) { + return null; + } + var hash = text.indexOf('#'); + if (hash > 0) { + text = text.substring(hash + 1); + } + return removeUri(text); + } +} diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java index 8ccbe46d4..6ef3e5a7f 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java @@ -17,6 +17,7 @@ import software.amazon.smithy.java.core.serde.TypeRegistry; import software.amazon.smithy.java.core.serde.document.DiscriminatorException; import software.amazon.smithy.java.core.serde.document.Document; +import software.amazon.smithy.java.core.serde.document.DocumentDeserializer; import software.amazon.smithy.java.http.api.HttpResponse; import software.amazon.smithy.java.io.datastream.DataStream; import software.amazon.smithy.model.shapes.ShapeId; @@ -116,7 +117,6 @@ default ModeledException createErrorFromDocument( * Different protocols need different parsers to extract the ShapeId given their different response structures. * If no parser specified, {@link #DEFAULT_ERROR_PAYLOAD_PARSER} will be picked. */ - @FunctionalInterface public interface ErrorPayloadParser { /** * This method should parse the response payload and extract error's ShapeId,and @@ -191,30 +191,45 @@ public ModeledException createErrorFromDocument( } }; - // This default parser should work for most protocols, but other protocols - // that do not support document types will need a custom parser to extract error ShapeId. - private static final ErrorPayloadParser DEFAULT_ERROR_PAYLOAD_PARSER = ( - Context context, - Codec codec, - KnownErrorFactory knownErrorFactory, - ShapeId serviceId, - TypeRegistry typeRegistry, - HttpResponse response, - ByteBuffer buffer) -> { - var document = codec.createDeserializer(buffer).readDocument(); - var id = document.parseErrorType(); - var builder = typeRegistry.createBuilder(id, ModeledException.class); - if (builder != null) { - return knownErrorFactory.createErrorFromDocument( - context, - codec, - response, - buffer, - document, - builder); + /** + * An implementation of ErrorPayloadParser which provides default payload parsing and error type extraction for protocols + * whose payload will be converted to a document. + */ + public static class DocumentPayloadParser implements ErrorPayloadParser { + public CallException parsePayload( + Context context, + Codec codec, + KnownErrorFactory knownErrorFactory, + ShapeId serviceId, + TypeRegistry typeRegistry, + HttpResponse response, + ByteBuffer buffer + ) { + var document = codec.createDeserializer(buffer).readDocument(); + var id = extractErrorType(document, serviceId.getNamespace()); + var builder = typeRegistry.createBuilder(id, ModeledException.class); + if (builder != null) { + return knownErrorFactory.createErrorFromDocument( + context, + codec, + response, + buffer, + document, + builder); + } + return null; } - return null; - }; + + public ShapeId extractErrorType(Document document, String namespace) { + return DocumentDeserializer.parseDiscriminator( + ErrorTypeUtils.removeNamespaceAndUri(ErrorTypeUtils.readTypeAndCode(document)), + namespace); + } + } + + // This default parser should work for most protocols, but other protocols + // that do not support document types will need a custom parser to extract error ShapeId. + private static final ErrorPayloadParser DEFAULT_ERROR_PAYLOAD_PARSER = new DocumentPayloadParser(); private final Codec codec; private final HeaderErrorExtractor headerErrorExtractor; diff --git a/client/client-http/src/test/java/software/amazon/smithy/java/client/http/ErrorTypeUtilsTest.java b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/ErrorTypeUtilsTest.java new file mode 100644 index 000000000..d038ad216 --- /dev/null +++ b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/ErrorTypeUtilsTest.java @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.client.http; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.java.core.serde.document.Document; + +public class ErrorTypeUtilsTest { + private static final String SIMPLE_TYPE = "FooError"; + private static final String TYPE_WITH_URI = "FooError:http://amazon.com/smithy/com.amazon.smithy.validate/"; + private static final String TYPE_WITH_NAMESPACE = "aws.protocoltests.restjson#FooError"; + private static final String TYPE_WITH_NAMESPACE_AND_URI = + "aws.protocoltests.restjson#FooError:http://amazon.com/smithy/com.amazon.smithy.validate/"; + + @Test + public void testRemoveUri() { + String expected = "FooError"; + String expectedWithNamespace = "aws.protocoltests.restjson#FooError"; + + assertEquals(expected, ErrorTypeUtils.removeUri(SIMPLE_TYPE)); + assertEquals(expected, ErrorTypeUtils.removeUri(TYPE_WITH_URI)); + assertEquals(expectedWithNamespace, ErrorTypeUtils.removeUri(TYPE_WITH_NAMESPACE)); + assertEquals(expectedWithNamespace, ErrorTypeUtils.removeUri(TYPE_WITH_NAMESPACE_AND_URI)); + } + + @Test + public void testRemoveNameSpaceAndUri() { + String expected = "FooError"; + + assertEquals(expected, ErrorTypeUtils.removeNamespaceAndUri(SIMPLE_TYPE)); + assertEquals(expected, ErrorTypeUtils.removeNamespaceAndUri(TYPE_WITH_URI)); + assertEquals(expected, ErrorTypeUtils.removeNamespaceAndUri(TYPE_WITH_NAMESPACE)); + assertEquals(expected, ErrorTypeUtils.removeNamespaceAndUri(TYPE_WITH_NAMESPACE_AND_URI)); + } + + @Test + public void testReadType() { + var document = Document.of(Map.of("__type", Document.of("foo"))); + assertEquals("foo", ErrorTypeUtils.readType(document)); + } + + @Test + public void testReadTypeAndCode() { + var document1 = Document.of(Map.of("__type", Document.of("foo"), "code", Document.of("bar"))); + var document2 = Document.of(Map.of("code", Document.of("bar"))); + + assertEquals("foo", ErrorTypeUtils.readTypeAndCode(document1)); + assertEquals("bar", ErrorTypeUtils.readTypeAndCode(document2)); + } +} diff --git a/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java b/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java index a93571e2b..9beebd41d 100644 --- a/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java +++ b/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java @@ -17,6 +17,7 @@ import software.amazon.smithy.java.client.core.ClientProtocol; import software.amazon.smithy.java.client.core.ClientProtocolFactory; import software.amazon.smithy.java.client.core.ProtocolSettings; +import software.amazon.smithy.java.client.http.ErrorTypeUtils; import software.amazon.smithy.java.client.http.HttpClientProtocol; import software.amazon.smithy.java.client.http.HttpErrorDeserializer; import software.amazon.smithy.java.context.Context; @@ -27,6 +28,8 @@ import software.amazon.smithy.java.core.schema.Unit; import software.amazon.smithy.java.core.serde.Codec; import software.amazon.smithy.java.core.serde.TypeRegistry; +import software.amazon.smithy.java.core.serde.document.Document; +import software.amazon.smithy.java.core.serde.document.DocumentDeserializer; import software.amazon.smithy.java.core.serde.event.EventDecoderFactory; import software.amazon.smithy.java.core.serde.event.EventEncoderFactory; import software.amazon.smithy.java.core.serde.event.EventStreamingException; @@ -51,7 +54,11 @@ public final class RpcV2CborProtocol extends HttpClientProtocol { public RpcV2CborProtocol(ShapeId service) { super(Rpcv2CborTrait.ID); this.service = service; - this.errorDeserializer = HttpErrorDeserializer.builder().codec(CBOR_CODEC).serviceId(service).build(); + this.errorDeserializer = HttpErrorDeserializer.builder() + .codec(CBOR_CODEC) + .serviceId(service) + .errorPayloadParser(new CborParser()) + .build(); } @Override @@ -163,6 +170,15 @@ private EventDecoderFactory getEventDecoderFactory( return AwsEventDecoderFactory.forOutputStream(outputOperation, payloadCodec(), f -> f); } + private static final class CborParser extends HttpErrorDeserializer.DocumentPayloadParser { + @Override + public ShapeId extractErrorType(Document document, String namespace) { + return DocumentDeserializer.parseDiscriminator( + ErrorTypeUtils.removeUri(ErrorTypeUtils.readType(document)), + namespace); + } + } + public static final class Factory implements ClientProtocolFactory { @Override public ShapeId id() { diff --git a/codecs/cbor-codec/src/main/java/software/amazon/smithy/java/cbor/CborDocuments.java b/codecs/cbor-codec/src/main/java/software/amazon/smithy/java/cbor/CborDocuments.java index 564cb7551..0344e8e32 100644 --- a/codecs/cbor-codec/src/main/java/software/amazon/smithy/java/cbor/CborDocuments.java +++ b/codecs/cbor-codec/src/main/java/software/amazon/smithy/java/cbor/CborDocuments.java @@ -13,7 +13,6 @@ import software.amazon.smithy.java.core.serde.ShapeSerializer; import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.java.core.serde.document.DocumentDeserializer; -import software.amazon.smithy.java.core.serde.document.DocumentUtils; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.utils.SmithyInternalApi; @@ -52,13 +51,11 @@ public ShapeType type() { @Override public ShapeId discriminator() { - var discriminator = extractType(); - return DocumentDeserializer.parseDiscriminator(discriminator, settings.defaultNamespace()); - } - - @Override - public ShapeId parseErrorType() { - var discriminator = DocumentUtils.removeUri(extractType()); + String discriminator = null; + var member = values.get("__type"); + if (member != null && member.type() == ShapeType.STRING) { + discriminator = member.asString(); + } return DocumentDeserializer.parseDiscriminator(discriminator, settings.defaultNamespace()); } @@ -105,14 +102,6 @@ public boolean equals(Object obj) { public int hashCode() { return values.hashCode(); } - - private String extractType() { - var member = values.get("__type"); - if (member != null && member.type() == ShapeType.STRING) { - return member.asString(); - } - return null; - } } /** diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java index f187589a2..d019456cb 100644 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonDocuments.java @@ -253,24 +253,6 @@ public ShapeId discriminator() { return DocumentDeserializer.parseDiscriminator(discriminator, settings.defaultNamespace()); } - @Override - public ShapeId parseErrorType() { - String errorType = null; - var typeMember = values.get("__type"); - if (typeMember != null && typeMember.type() == ShapeType.STRING) { - errorType = typeMember.asString(); - } else { - var codeMember = values.get("code"); - if (codeMember != null && codeMember.type() == ShapeType.STRING) { - errorType = codeMember.asString(); - } - } - if (settings.errorTypeSanitizer() != null) { - errorType = settings.errorTypeSanitizer().apply(errorType); - } - return DocumentDeserializer.parseDiscriminator(errorType, settings.defaultNamespace()); - } - @Override public Map asStringMap() { return values; diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java b/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java index 5d6986365..2f4dd45f0 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java @@ -125,18 +125,6 @@ default ShapeId discriminator() { return null; } - /** - * Attempts to find and parse a sanitized shape ID from the document in the document's discriminator field. - * - *

The discriminator of an error can be represented by either the __type field or the code field - * based on the protocol. - * - * @return the parsed shape ID, or null if not found. - */ - default ShapeId parseErrorType() { - return null; - } - /** * Serializes the Document as a document value in the Smithy data model. * diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/document/DocumentUtils.java b/core/src/main/java/software/amazon/smithy/java/core/serde/document/DocumentUtils.java index 625057ca9..7e8730f19 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/document/DocumentUtils.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/document/DocumentUtils.java @@ -175,44 +175,4 @@ public static T getMemberValue(Document container, Schema containerSchema, S .id() + "`: " + e.getMessage()); } } - - /** - * Removes the trailing URI in {@code __type} field or {@code code} field of the document - * - *

For example, given {@code __type = "aws.protocoltests.restjson#FooError:http://abc.com"}, - * protocols like restJSON should ignore the trailing URI and keep the namespace of the error type. - * - * @param text The error type string. - * @return The error type string without the trailing URI. - */ - public static String removeUri(String text) { - if (text == null) { - return null; - } - var colon = text.indexOf(':'); - if (colon > 0) { - text = text.substring(0, colon); - } - return text; - } - - /** - * Removes the namespace and trailing URI in {@code __type} field or {@code code} field of the document - * - *

For example, given {@code __type = "aws.protocoltests.restjson#FooError:http://abc.com"}, - * protocols like awsJSON 1.1 should ignore the namespace and the trailing URI of the error type. - * - * @param text The error type string. - * @return The error type string without the trailing URI. - */ - public static String removeNamespaceAndUri(String text) { - if (text == null) { - return null; - } - var hash = text.indexOf('#'); - if (hash > 0) { - text = text.substring(hash + 1); - } - return removeUri(text); - } } diff --git a/core/src/test/java/software/amazon/smithy/java/core/serde/document/DocumentUtilsTest.java b/core/src/test/java/software/amazon/smithy/java/core/serde/document/DocumentUtilsTest.java deleted file mode 100644 index 2f30cd1ac..000000000 --- a/core/src/test/java/software/amazon/smithy/java/core/serde/document/DocumentUtilsTest.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.java.core.serde.document; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -public class DocumentUtilsTest { - private static final String SIMPLE_TYPE = "FooError"; - private static final String TYPE_WITH_URI = "FooError:http://amazon.com/smithy/com.amazon.smithy.validate/"; - private static final String TYPE_WITH_NAMESPACE = "aws.protocoltests.restjson#FooError"; - private static final String TYPE_WITH_NAMESPACE_AND_URI = - "aws.protocoltests.restjson#FooError:http://amazon.com/smithy/com.amazon.smithy.validate/"; - - @Test - public void testRemoveUri() { - String expected = "FooError"; - String expectedWithNamespace = "aws.protocoltests.restjson#FooError"; - assertEquals(expected, DocumentUtils.removeUri(SIMPLE_TYPE)); - assertEquals(expected, DocumentUtils.removeUri(TYPE_WITH_URI)); - assertEquals(expectedWithNamespace, DocumentUtils.removeUri(TYPE_WITH_NAMESPACE)); - assertEquals(expectedWithNamespace, DocumentUtils.removeUri(TYPE_WITH_NAMESPACE_AND_URI)); - } - - @Test - public void testRemoveNameSpaceAndUri() { - String expected = "FooError"; - assertEquals(expected, DocumentUtils.removeNamespaceAndUri(SIMPLE_TYPE)); - assertEquals(expected, DocumentUtils.removeNamespaceAndUri(TYPE_WITH_URI)); - assertEquals(expected, DocumentUtils.removeNamespaceAndUri(TYPE_WITH_NAMESPACE)); - assertEquals(expected, DocumentUtils.removeNamespaceAndUri(TYPE_WITH_NAMESPACE_AND_URI)); - } -} From d1797357c3b2acd1caea80b62ba1fb14774a6785 Mon Sep 17 00:00:00 2001 From: Joe Wu Date: Mon, 8 Dec 2025 13:36:43 -0800 Subject: [PATCH 7/7] Remove redundant code, modify ErrorPayloadParser interface to make protocol based parser not stateful --- .../aws/client/awsjson/AwsJson11Protocol.java | 2 +- .../aws/client/awsjson/AwsJson1Protocol.java | 14 ++-- .../client/restxml/RestXmlClientProtocol.java | 48 ++++++++----- .../java/client/http/ErrorTypeUtils.java | 4 +- .../client/http/HttpErrorDeserializer.java | 72 +++++++++---------- .../java/client/http/ErrorTypeUtilsTest.java | 8 +++ .../java/client/rpcv2/RpcV2CborProtocol.java | 13 ++-- .../amazon/smithy/java/json/JsonCodec.java | 12 ---- .../amazon/smithy/java/json/JsonSettings.java | 25 ------- 9 files changed, 85 insertions(+), 113 deletions(-) diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java index ba8d654dc..cee547540 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson11Protocol.java @@ -25,7 +25,7 @@ public final class AwsJson11Protocol extends AwsJsonProtocol { * discriminator of documents that use relative shape IDs. */ public AwsJson11Protocol(ShapeId service) { - super(TRAIT_ID, service, new HttpErrorDeserializer.DocumentPayloadParser()); + super(TRAIT_ID, service, HttpErrorDeserializer.DEFAULT_ERROR_PAYLOAD_PARSER); } @Override diff --git a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java index 5e781a617..b28b6b639 100644 --- a/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java +++ b/aws/client/aws-client-awsjson/src/main/java/software/amazon/smithy/java/aws/client/awsjson/AwsJson1Protocol.java @@ -11,7 +11,6 @@ import software.amazon.smithy.java.client.core.ClientProtocolFactory; import software.amazon.smithy.java.client.core.ProtocolSettings; import software.amazon.smithy.java.client.http.ErrorTypeUtils; -import software.amazon.smithy.java.client.http.HttpErrorDeserializer; import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.java.core.serde.document.DocumentDeserializer; import software.amazon.smithy.model.shapes.ShapeId; @@ -28,16 +27,13 @@ public final class AwsJson1Protocol extends AwsJsonProtocol { * discriminator of documents that use relative shape IDs. */ public AwsJson1Protocol(ShapeId service) { - super(TRAIT_ID, service, new Json10Parser()); + super(TRAIT_ID, service, AwsJson1Protocol::extractErrorType); } - private static final class Json10Parser extends HttpErrorDeserializer.DocumentPayloadParser { - @Override - public ShapeId extractErrorType(Document document, String namespace) { - return DocumentDeserializer.parseDiscriminator( - ErrorTypeUtils.removeUri(ErrorTypeUtils.readTypeAndCode(document)), - namespace); - } + private static ShapeId extractErrorType(Document document, String namespace) { + return DocumentDeserializer.parseDiscriminator( + ErrorTypeUtils.removeUri(ErrorTypeUtils.readTypeAndCode(document)), + namespace); } @Override diff --git a/aws/client/aws-client-restxml/src/main/java/software/amazon/smithy/java/aws/client/restxml/RestXmlClientProtocol.java b/aws/client/aws-client-restxml/src/main/java/software/amazon/smithy/java/aws/client/restxml/RestXmlClientProtocol.java index 40fda578e..32f87cbf7 100644 --- a/aws/client/aws-client-restxml/src/main/java/software/amazon/smithy/java/aws/client/restxml/RestXmlClientProtocol.java +++ b/aws/client/aws-client-restxml/src/main/java/software/amazon/smithy/java/aws/client/restxml/RestXmlClientProtocol.java @@ -18,11 +18,13 @@ import software.amazon.smithy.java.client.http.binding.HttpBindingClientProtocol; import software.amazon.smithy.java.client.http.binding.HttpBindingErrorFactory; import software.amazon.smithy.java.context.Context; +import software.amazon.smithy.java.core.error.CallException; import software.amazon.smithy.java.core.error.ModeledException; import software.amazon.smithy.java.core.schema.InputEventStreamingApiOperation; import software.amazon.smithy.java.core.schema.OutputEventStreamingApiOperation; import software.amazon.smithy.java.core.serde.Codec; import software.amazon.smithy.java.core.serde.TypeRegistry; +import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.java.core.serde.event.EventDecoderFactory; import software.amazon.smithy.java.core.serde.event.EventEncoderFactory; import software.amazon.smithy.java.core.serde.event.EventStreamingException; @@ -95,24 +97,34 @@ protected EventDecoderFactory getEventDecoderFactory( return AwsEventDecoderFactory.forOutputStream(outputOperation, payloadCodec(), f -> f); } - private static final HttpErrorDeserializer.ErrorPayloadParser XML_ERROR_PAYLOAD_PARSER = ( - Context context, - Codec codec, - HttpErrorDeserializer.KnownErrorFactory knownErrorFactory, - ShapeId serviceId, - TypeRegistry typeRegistry, - HttpResponse response, - ByteBuffer buffer) -> { - var deserializer = codec.createDeserializer(buffer); - String code = XmlUtil.parseErrorCodeName(deserializer); - var nameSpace = serviceId.getNamespace(); - var id = ShapeId.fromOptionalNamespace(nameSpace, code); - var builder = typeRegistry.createBuilder(id, ModeledException.class); - if (builder != null) { - return knownErrorFactory.createError(context, codec, response, builder); - } - return null; - }; + private static final HttpErrorDeserializer.ErrorPayloadParser XML_ERROR_PAYLOAD_PARSER = + new HttpErrorDeserializer.ErrorPayloadParser() { + @Override + public CallException parsePayload( + Context context, + Codec codec, + HttpErrorDeserializer.KnownErrorFactory knownErrorFactory, + ShapeId serviceId, + TypeRegistry typeRegistry, + HttpResponse response, + ByteBuffer buffer + ) { + var deserializer = codec.createDeserializer(buffer); + String code = XmlUtil.parseErrorCodeName(deserializer); + var nameSpace = serviceId.getNamespace(); + var id = ShapeId.fromOptionalNamespace(nameSpace, code); + var builder = typeRegistry.createBuilder(id, ModeledException.class); + if (builder != null) { + return knownErrorFactory.createError(context, codec, response, builder); + } + return null; + } + + @Override + public ShapeId extractErrorType(Document document, String namespace) { + return null; + } + }; public static final class Factory implements ClientProtocolFactory { @Override diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/ErrorTypeUtils.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/ErrorTypeUtils.java index d67681c37..34892bc0f 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/ErrorTypeUtils.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/ErrorTypeUtils.java @@ -8,7 +8,9 @@ import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.model.shapes.ShapeType; -public class ErrorTypeUtils { +public final class ErrorTypeUtils { + + private ErrorTypeUtils() {} /** * Read the error type from __type field of the document. diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java index 6ef3e5a7f..f790a5e45 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpErrorDeserializer.java @@ -117,6 +117,7 @@ default ModeledException createErrorFromDocument( * Different protocols need different parsers to extract the ShapeId given their different response structures. * If no parser specified, {@link #DEFAULT_ERROR_PAYLOAD_PARSER} will be picked. */ + @FunctionalInterface public interface ErrorPayloadParser { /** * This method should parse the response payload and extract error's ShapeId,and @@ -132,7 +133,7 @@ public interface ErrorPayloadParser { * * @return the created error. */ - CallException parsePayload( + default CallException parsePayload( Context context, Codec codec, KnownErrorFactory knownErrorFactory, @@ -140,7 +141,33 @@ CallException parsePayload( TypeRegistry typeRegistry, HttpResponse response, ByteBuffer buffer - ) throws SerializationException, DiscriminatorException; + ) { + var document = codec.createDeserializer(buffer).readDocument(); + var id = extractErrorType(document, serviceId.getNamespace()); + var builder = typeRegistry.createBuilder(id, ModeledException.class); + if (builder != null) { + return knownErrorFactory.createErrorFromDocument( + context, + codec, + response, + buffer, + document, + builder); + } + return null; + } + + /** + * This method should extract the error type from converted document based on the + * protocol requirement from either __type or code and apply necessary sanitizing to + * the error type. + * + * @param document The converted document of the payload. + * @param namespace The default namespace used for error type's ShapeId. + * + * @return the created error. + */ + ShapeId extractErrorType(Document document, String namespace); } // Does not check for any error headers by default. @@ -191,45 +218,12 @@ public ModeledException createErrorFromDocument( } }; - /** - * An implementation of ErrorPayloadParser which provides default payload parsing and error type extraction for protocols - * whose payload will be converted to a document. - */ - public static class DocumentPayloadParser implements ErrorPayloadParser { - public CallException parsePayload( - Context context, - Codec codec, - KnownErrorFactory knownErrorFactory, - ShapeId serviceId, - TypeRegistry typeRegistry, - HttpResponse response, - ByteBuffer buffer - ) { - var document = codec.createDeserializer(buffer).readDocument(); - var id = extractErrorType(document, serviceId.getNamespace()); - var builder = typeRegistry.createBuilder(id, ModeledException.class); - if (builder != null) { - return knownErrorFactory.createErrorFromDocument( - context, - codec, - response, - buffer, - document, - builder); - } - return null; - } - - public ShapeId extractErrorType(Document document, String namespace) { - return DocumentDeserializer.parseDiscriminator( - ErrorTypeUtils.removeNamespaceAndUri(ErrorTypeUtils.readTypeAndCode(document)), - namespace); - } - } - // This default parser should work for most protocols, but other protocols // that do not support document types will need a custom parser to extract error ShapeId. - private static final ErrorPayloadParser DEFAULT_ERROR_PAYLOAD_PARSER = new DocumentPayloadParser(); + public static final ErrorPayloadParser DEFAULT_ERROR_PAYLOAD_PARSER = + (document, namespace) -> DocumentDeserializer.parseDiscriminator( + ErrorTypeUtils.removeNamespaceAndUri(ErrorTypeUtils.readTypeAndCode(document)), + namespace); private final Codec codec; private final HeaderErrorExtractor headerErrorExtractor; diff --git a/client/client-http/src/test/java/software/amazon/smithy/java/client/http/ErrorTypeUtilsTest.java b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/ErrorTypeUtilsTest.java index d038ad216..22a341c47 100644 --- a/client/client-http/src/test/java/software/amazon/smithy/java/client/http/ErrorTypeUtilsTest.java +++ b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/ErrorTypeUtilsTest.java @@ -6,6 +6,7 @@ package software.amazon.smithy.java.client.http; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import java.util.Map; import org.junit.jupiter.api.Test; @@ -53,4 +54,11 @@ public void testReadTypeAndCode() { assertEquals("foo", ErrorTypeUtils.readTypeAndCode(document1)); assertEquals("bar", ErrorTypeUtils.readTypeAndCode(document2)); } + + @Test + public void testWrongTypeIgnored() { + var document = Document.of(Map.of("__type", Document.of(123))); + + assertNull(ErrorTypeUtils.readType(document)); + } } diff --git a/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java b/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java index 9beebd41d..2211a0ebc 100644 --- a/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java +++ b/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java @@ -57,7 +57,7 @@ public RpcV2CborProtocol(ShapeId service) { this.errorDeserializer = HttpErrorDeserializer.builder() .codec(CBOR_CODEC) .serviceId(service) - .errorPayloadParser(new CborParser()) + .errorPayloadParser(RpcV2CborProtocol::extractErrorType) .build(); } @@ -170,13 +170,10 @@ private EventDecoderFactory getEventDecoderFactory( return AwsEventDecoderFactory.forOutputStream(outputOperation, payloadCodec(), f -> f); } - private static final class CborParser extends HttpErrorDeserializer.DocumentPayloadParser { - @Override - public ShapeId extractErrorType(Document document, String namespace) { - return DocumentDeserializer.parseDiscriminator( - ErrorTypeUtils.removeUri(ErrorTypeUtils.readType(document)), - namespace); - } + private static ShapeId extractErrorType(Document document, String namespace) { + return DocumentDeserializer.parseDiscriminator( + ErrorTypeUtils.removeUri(ErrorTypeUtils.readType(document)), + namespace); } public static final class Factory implements ClientProtocolFactory { diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonCodec.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonCodec.java index 5070b0b46..3c61450bd 100644 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonCodec.java +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonCodec.java @@ -7,7 +7,6 @@ import java.io.OutputStream; import java.nio.ByteBuffer; -import java.util.function.Function; import software.amazon.smithy.java.core.serde.Codec; import software.amazon.smithy.java.core.serde.ShapeDeserializer; import software.amazon.smithy.java.core.serde.ShapeSerializer; @@ -175,16 +174,5 @@ Builder overrideSerdeProvider(JsonSerdeProvider provider) { settingsBuilder.overrideSerdeProvider(provider); return this; } - - /** - * Uses a custom error type sanitizer to process the {@code __type} field. - * - * @param errorTypeSanitizer the type sanitizer to use. - * @return the builder. - */ - public Builder errorTypeSanitizer(Function errorTypeSanitizer) { - settingsBuilder.errorTypeSanitizer(errorTypeSanitizer); - return this; - } } } diff --git a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java index fb6beeb26..437dbd1ea 100644 --- a/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java +++ b/codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java @@ -7,7 +7,6 @@ import java.util.Objects; import java.util.ServiceLoader; -import java.util.function.Function; import software.amazon.smithy.java.core.serde.TimestampFormatter; /** @@ -46,7 +45,6 @@ public final class JsonSettings { private final JsonSerdeProvider provider; private final boolean serializeTypeInDocuments; private final boolean prettyPrint; - private final Function errorTypeSanitizer; private JsonSettings(Builder builder) { this.timestampResolver = builder.useTimestampFormat @@ -60,7 +58,6 @@ private JsonSettings(Builder builder) { this.provider = builder.provider; this.serializeTypeInDocuments = builder.serializeTypeInDocuments; this.prettyPrint = builder.prettyPrint; - this.errorTypeSanitizer = builder.errorTypeSanitizer; } /** @@ -118,16 +115,6 @@ public boolean serializeTypeInDocuments() { return serializeTypeInDocuments; } - /** - * The error type sanitizer to use for {@code __type} or {@code code} when parsing the error discriminator. - * Default is null - * - * @return the sanitizer used or null - */ - public Function errorTypeSanitizer() { - return errorTypeSanitizer; - } - /** * Whether to format the JSON output with pretty printing (indentation and line breaks). * @@ -178,7 +165,6 @@ public static final class Builder { private JsonSerdeProvider provider = PROVIDER; private boolean serializeTypeInDocuments = true; private boolean prettyPrint = false; - private Function errorTypeSanitizer; private Builder() {} @@ -289,16 +275,5 @@ Builder overrideSerdeProvider(JsonSerdeProvider provider) { this.provider = Objects.requireNonNull(provider); return this; } - - /** - * Uses a custom error type sanitizer for error type - * - * @param errorTypeSanitizer the sanitizer to use for error type. - * @return the builder. - */ - public Builder errorTypeSanitizer(Function errorTypeSanitizer) { - this.errorTypeSanitizer = errorTypeSanitizer; - return this; - } } }