From ef1e3b7fa8afb240b4472ed3b5a5ec7255bf8040 Mon Sep 17 00:00:00 2001 From: George Fu Date: Wed, 31 Jan 2024 20:35:53 +0000 Subject: [PATCH 1/2] feat: generate unified error dispatcher --- .changeset/shy-nails-wonder.md | 2 + .../HttpBindingProtocolGenerator.java | 22 ++++++--- .../HttpProtocolGeneratorUtils.java | 47 +++++++++---------- .../integration/HttpRpcProtocolGenerator.java | 22 +++++---- .../integration/ProtocolGenerator.java | 18 +++++++ 5 files changed, 72 insertions(+), 39 deletions(-) create mode 100644 .changeset/shy-nails-wonder.md diff --git a/.changeset/shy-nails-wonder.md b/.changeset/shy-nails-wonder.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/shy-nails-wonder.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java index e2d9118a3f0..9725f29f186 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java @@ -517,6 +517,7 @@ public void generateResponseDeserializers(GenerationContext context) { Set containedOperations = new TreeSet<>( topDownIndex.getContainedOperations(context.getService())); + for (OperationShape operation : containedOperations) { OptionalUtils.ifPresentOrElse( operation.getTrait(HttpTrait.class), @@ -525,6 +526,18 @@ public void generateResponseDeserializers(GenerationContext context) { "Unable to generate %s protocol response bindings for %s because it does not have an " + "http binding trait", getName(), operation.getId()))); } + + SymbolReference responseType = getApplicationProtocol().getResponseType(); + Set errorShapes = HttpProtocolGeneratorUtils.generateUnifiedErrorDispatcher( + context, + containedOperations.stream().toList(), + responseType, + this::writeErrorCodeParser, + isErrorCodeInBody, + this::getErrorBodyLocation, + this::getOperationErrors + ); + deserializingErrorShapes.addAll(errorShapes); } private void generateOperationResponseSerializer( @@ -2091,7 +2104,7 @@ private void generateOperationResponseDeserializer( // e.g., deserializeAws_restJson1_1ExecuteStatement String methodName = ProtocolGenerator.getDeserFunctionShortName(symbol); String methodLongName = ProtocolGenerator.getDeserFunctionName(symbol, getName()); - String errorMethodName = methodName + "Error"; + String errorMethodName = "de_CommandError"; // Add the normalized output type. Symbol outputType = symbol.expectProperty("outputType", Symbol.class); String contextType = CodegenUtils.getOperationDeserializerContextType(context.getSettings(), writer, @@ -2108,7 +2121,7 @@ private void generateOperationResponseDeserializer( // status code that's not the modeled code (300 or higher). This allows for // returning other 2XX codes that don't match the defined value. writer.openBlock("if (output.statusCode !== $L && output.statusCode >= 300) {", "}", trait.getCode(), - () -> writer.write("return $L(output, context);", errorMethodName)); + () -> writer.write("return $L(output, context) as any;", errorMethodName)); // Start deserializing the response. writer.openBlock("const contents: any = map({", "});", () -> { @@ -2129,11 +2142,6 @@ private void generateOperationResponseDeserializer( writer.write("return contents;"); }); writer.write(""); - // Write out the error deserialization dispatcher. - Set errorShapes = HttpProtocolGeneratorUtils.generateErrorDispatcher( - context, operation, responseType, this::writeErrorCodeParser, - isErrorCodeInBody, this::getErrorBodyLocation, this::getOperationErrors); - deserializingErrorShapes.addAll(errorShapes); } private void generateErrorDeserializer(GenerationContext context, StructureShape error) { diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpProtocolGeneratorUtils.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpProtocolGeneratorUtils.java index 6ade0850824..723759eb045 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpProtocolGeneratorUtils.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpProtocolGeneratorUtils.java @@ -298,12 +298,11 @@ public static void writeRetryableTrait(TypeScriptWriter writer, StructureShape e /** * Writes a function used to dispatch to the proper error deserializer - * for each error that the operation can return. The generated function + * for each error that any operation can return. The generated function * assumes a deserialization function is generated for the structures * returned. * * @param context The generation context. - * @param operation The operation to generate for. * @param responseType The response type for the HTTP protocol. * @param errorCodeGenerator A consumer * @param shouldParseErrorBody Flag indicating whether need to parse response body in this dispatcher function @@ -311,38 +310,37 @@ public static void writeRetryableTrait(TypeScriptWriter writer, StructureShape e * @param operationErrorsToShapes A map of error names to their {@link ShapeId}. * @return A set of all error structure shapes for the operation that were dispatched to. */ - static Set generateErrorDispatcher( - GenerationContext context, - OperationShape operation, - SymbolReference responseType, - Consumer errorCodeGenerator, - boolean shouldParseErrorBody, - BiFunction bodyErrorLocationModifier, - BiFunction> operationErrorsToShapes + static Set generateUnifiedErrorDispatcher( + GenerationContext context, + List operations, + SymbolReference responseType, + Consumer errorCodeGenerator, + boolean shouldParseErrorBody, + BiFunction bodyErrorLocationModifier, + BiFunction, Map> operationErrorsToShapes ) { TypeScriptWriter writer = context.getWriter(); SymbolProvider symbolProvider = context.getSymbolProvider(); Set errorShapes = new TreeSet<>(); - Symbol symbol = symbolProvider.toSymbol(operation); - Symbol outputType = symbol.expectProperty("outputType", Symbol.class); - String errorMethodName = ProtocolGenerator.getDeserFunctionShortName(symbol) + "Error"; - String errorMethodLongName = ProtocolGenerator.getDeserFunctionName(symbol, context.getProtocolName()) - + "Error"; + String errorMethodName = "de_CommandError"; + String errorMethodLongName = "deserialize_" + + ProtocolGenerator.getSanitizedName(context.getProtocolName()) + + "CommandError"; writer.writeDocs(errorMethodLongName); - writer.openBlock("const $L = async(\n" - + " output: $T,\n" - + " context: __SerdeContext,\n" - + "): Promise<$T> => {", "}", errorMethodName, responseType, outputType, () -> { + writer.openBlock("const $L = async(\n" + + " output: $T,\n" + + " context: __SerdeContext,\n" + + "): Promise => {", "}", errorMethodName, responseType, () -> { // Prepare error response for parsing error code. If error code needs to be parsed from response body // then we collect body and parse it to JS object, otherwise leave the response body as is. if (shouldParseErrorBody) { writer.openBlock("const parsedOutput: any = {", "};", - () -> { - writer.write("...output,"); - writer.write("body: await parseErrorBody(output.body, context)"); - }); + () -> { + writer.write("...output,"); + writer.write("body: await parseErrorBody(output.body, context)"); + }); } // Error responses must be at least BaseException interface @@ -370,7 +368,8 @@ static Set generateErrorDispatcher( }); }; - Map operationNamesToShapes = operationErrorsToShapes.apply(context, operation); + Map operationNamesToShapes = operationErrorsToShapes.apply(context, operations); + if (!operationNamesToShapes.isEmpty()) { writer.openBlock("switch (errorCode) {", "}", () -> { // Generate the case statement for each error, invoking the specific deserializer. diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java index 366bcd658c5..1bc2f29f6b0 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java @@ -224,6 +224,18 @@ public void generateResponseDeserializers(GenerationContext context) { for (OperationShape operation : containedOperations) { generateOperationDeserializer(context, operation); } + + SymbolReference responseType = getApplicationProtocol().getResponseType(); + Set errorShapes = HttpProtocolGeneratorUtils.generateUnifiedErrorDispatcher( + context, + containedOperations.stream().toList(), + responseType, + this::writeErrorCodeParser, + isErrorCodeInBody, + this::getErrorBodyLocation, + this::getOperationErrors + ); + deserializingErrorShapes.addAll(errorShapes); } private void generateOperationSerializer(GenerationContext context, OperationShape operation) { @@ -435,7 +447,7 @@ private void generateOperationDeserializer(GenerationContext context, OperationS // e.g., deserializeAws_restJson1_1ExecuteStatement String methodName = ProtocolGenerator.getDeserFunctionShortName(symbol); String methodLongName = ProtocolGenerator.getDeserFunctionName(symbol, getName()); - String errorMethodName = methodName + "Error"; + String errorMethodName = "de_CommandError"; String serdeContextType = CodegenUtils.getOperationDeserializerContextType(context.getSettings(), writer, context.getModel(), operation); // Add the normalized output type. @@ -449,7 +461,7 @@ private void generateOperationDeserializer(GenerationContext context, OperationS + "): Promise<$T> => {", "}", methodName, responseType, serdeContextType, outputType, () -> { // Redirect error deserialization to the dispatcher writer.openBlock("if (output.statusCode >= 300) {", "}", () -> { - writer.write("return $L(output, context);", errorMethodName); + writer.write("return $L(output, context) as any;", errorMethodName); }); // Start deserializing the response. @@ -465,12 +477,6 @@ private void generateOperationDeserializer(GenerationContext context, OperationS writer.write("return response;"); }); writer.write(""); - - // Write out the error deserialization dispatcher. - Set errorShapes = HttpProtocolGeneratorUtils.generateErrorDispatcher( - context, operation, responseType, this::writeErrorCodeParser, - isErrorCodeInBody, this::getErrorBodyLocation, this::getOperationErrors); - deserializingErrorShapes.addAll(errorShapes); } private void generateErrorDeserializer(GenerationContext context, StructureShape error) { diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/ProtocolGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/ProtocolGenerator.java index 892064207c2..31f6607567d 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/ProtocolGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/ProtocolGenerator.java @@ -16,6 +16,7 @@ package software.amazon.smithy.typescript.codegen.integration; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; import software.amazon.smithy.codegen.core.CodegenException; @@ -303,6 +304,23 @@ default Map getOperationErrors(GenerationContext context, Opera return HttpProtocolGeneratorUtils.getOperationErrors(context, operation); } + /** + * Returns a map of error names to their {@link ShapeId}. + * + * @param context the generation context + * @param operations the operation shapes to retrieve errors for + * @return map of error names to {@link ShapeId} + */ + default Map getOperationErrors(GenerationContext context, Collection operations) { + Map errors = new LinkedHashMap<>(); + for (OperationShape operation : operations) { + errors.putAll( + getOperationErrors(context, operation) + ); + } + return errors; + } + /** * Context object used for service serialization and deserialization. */ From 73e007120aa5b7329cafe91d2d6aa4cf73b34aa9 Mon Sep 17 00:00:00 2001 From: George Fu Date: Mon, 5 Feb 2024 16:24:51 +0000 Subject: [PATCH 2/2] change Promise to Promise --- .../codegen/integration/HttpBindingProtocolGenerator.java | 2 +- .../codegen/integration/HttpProtocolGeneratorUtils.java | 2 +- .../codegen/integration/HttpRpcProtocolGenerator.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java index 9725f29f186..410704da451 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java @@ -2121,7 +2121,7 @@ private void generateOperationResponseDeserializer( // status code that's not the modeled code (300 or higher). This allows for // returning other 2XX codes that don't match the defined value. writer.openBlock("if (output.statusCode !== $L && output.statusCode >= 300) {", "}", trait.getCode(), - () -> writer.write("return $L(output, context) as any;", errorMethodName)); + () -> writer.write("return $L(output, context);", errorMethodName)); // Start deserializing the response. writer.openBlock("const contents: any = map({", "});", () -> { diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpProtocolGeneratorUtils.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpProtocolGeneratorUtils.java index 723759eb045..eeeea2fc6dc 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpProtocolGeneratorUtils.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpProtocolGeneratorUtils.java @@ -332,7 +332,7 @@ static Set generateUnifiedErrorDispatcher( writer.openBlock("const $L = async(\n" + " output: $T,\n" + " context: __SerdeContext,\n" - + "): Promise => {", "}", errorMethodName, responseType, () -> { + + "): Promise => {", "}", errorMethodName, responseType, () -> { // Prepare error response for parsing error code. If error code needs to be parsed from response body // then we collect body and parse it to JS object, otherwise leave the response body as is. if (shouldParseErrorBody) { diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java index 1bc2f29f6b0..a799f7d713d 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java @@ -461,7 +461,7 @@ private void generateOperationDeserializer(GenerationContext context, OperationS + "): Promise<$T> => {", "}", methodName, responseType, serdeContextType, outputType, () -> { // Redirect error deserialization to the dispatcher writer.openBlock("if (output.statusCode >= 300) {", "}", () -> { - writer.write("return $L(output, context) as any;", errorMethodName); + writer.write("return $L(output, context);", errorMethodName); }); // Start deserializing the response.