diff --git a/.gitignore b/.gitignore index 0884fab65aa..eb480ecfdc0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ target/ build/ */out/ */*/out/ +wrapper/ # Visual Studio Code -.vscode/* \ No newline at end of file +.vscode/* diff --git a/build.gradle.kts b/build.gradle.kts index cf43b0636b5..698c115a6b8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ plugins { allprojects { group = "software.amazon.smithy" - version = "0.2.0" + version = "0.3.0" } // The root project doesn't produce a JAR. diff --git a/smithy-typescript-codegen-test/build.gradle.kts b/smithy-typescript-codegen-test/build.gradle.kts index bff567c77da..6b389e4d741 100644 --- a/smithy-typescript-codegen-test/build.gradle.kts +++ b/smithy-typescript-codegen-test/build.gradle.kts @@ -19,7 +19,7 @@ extra["moduleName"] = "software.amazon.smithy.typescript.codegen.test" tasks["jar"].enabled = false plugins { - id("software.amazon.smithy").version("0.5.1") + id("software.amazon.smithy").version("0.5.2") } repositories { @@ -29,5 +29,5 @@ repositories { dependencies { implementation(project(":smithy-typescript-codegen")) - implementation("software.amazon.smithy:smithy-protocol-test-traits:[1.0.8, 2.0[") + implementation("software.amazon.smithy:smithy-protocol-test-traits:[1.5.0, 2.0[") } diff --git a/smithy-typescript-codegen-test/model/main.smithy b/smithy-typescript-codegen-test/model/main.smithy index ddf8822c794..43058228445 100644 --- a/smithy-typescript-codegen-test/model/main.smithy +++ b/smithy-typescript-codegen-test/model/main.smithy @@ -259,6 +259,7 @@ union Precipitation { structure OtherStructure {} +@suppress(["EnumNamesPresent"]) @enum([{value: "YES"}, {value: "NO"}]) string SimpleYesNo diff --git a/smithy-typescript-codegen/build.gradle.kts b/smithy-typescript-codegen/build.gradle.kts index 6583d0a65e9..baa6f66d60d 100644 --- a/smithy-typescript-codegen/build.gradle.kts +++ b/smithy-typescript-codegen/build.gradle.kts @@ -18,6 +18,6 @@ extra["displayName"] = "Smithy :: Typescript :: Codegen" extra["moduleName"] = "software.amazon.smithy.typescript.codegen" dependencies { - api("software.amazon.smithy:smithy-codegen-core:[1.0.8, 2.0[") - implementation("software.amazon.smithy:smithy-protocol-test-traits:[1.0.8, 2.0[") + api("software.amazon.smithy:smithy-codegen-core:[1.5.0, 2.0[") + implementation("software.amazon.smithy:smithy-protocol-test-traits:[1.5.0, 2.0[") } diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/HttpProtocolTestGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/HttpProtocolTestGenerator.java index 8733d622fe7..de969d93881 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/HttpProtocolTestGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/HttpProtocolTestGenerator.java @@ -51,12 +51,14 @@ import software.amazon.smithy.model.traits.ErrorTrait; import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait; import software.amazon.smithy.model.traits.IdempotencyTokenTrait; +import software.amazon.smithy.model.traits.StreamingTrait; import software.amazon.smithy.protocoltests.traits.HttpMessageTestCase; import software.amazon.smithy.protocoltests.traits.HttpRequestTestCase; import software.amazon.smithy.protocoltests.traits.HttpRequestTestsTrait; import software.amazon.smithy.protocoltests.traits.HttpResponseTestCase; import software.amazon.smithy.protocoltests.traits.HttpResponseTestsTrait; import software.amazon.smithy.utils.IoUtils; +import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.MapUtils; import software.amazon.smithy.utils.Pair; @@ -77,6 +79,10 @@ final class HttpProtocolTestGenerator implements Runnable { private static final Logger LOGGER = Logger.getLogger(HttpProtocolTestGenerator.class.getName()); private static final String TEST_CASE_FILE_TEMPLATE = "tests/functional/%s.spec.ts"; + // Headers that are generated by other sources that need to be normalized in the + // test cases to match. + private static final List HEADERS_TO_NORMALIZE = ListUtils.of("content-length", "content-type"); + private final TypeScriptSettings settings; private final Model model; private final ShapeId protocol; @@ -248,21 +254,31 @@ private void writeRequestQueryAssertions(HttpRequestTestCase testCase) { } private void writeRequestHeaderAssertions(HttpRequestTestCase testCase) { - testCase.getRequireHeaders().forEach(requiredHeader -> - writer.write("expect(r.headers[$S]).toBeDefined();", requiredHeader)); + testCase.getRequireHeaders().forEach(requiredHeader -> { + writer.write("expect(r.headers[$S]).toBeDefined();", normalizeHeaderName(requiredHeader)); + }); writer.write(""); testCase.getForbidHeaders().forEach(forbidHeader -> - writer.write("expect(r.headers[$S]).toBeUndefined();", forbidHeader)); + writer.write("expect(r.headers[$S]).toBeUndefined();", normalizeHeaderName(forbidHeader))); writer.write(""); testCase.getHeaders().forEach((header, value) -> { + header = normalizeHeaderName(header); writer.write("expect(r.headers[$S]).toBeDefined();", header); writer.write("expect(r.headers[$S]).toBe($S);", header, value); }); writer.write(""); } + private String normalizeHeaderName(String headerName) { + if (HEADERS_TO_NORMALIZE.contains(headerName.toLowerCase())) { + return headerName.toLowerCase(); + } + + return headerName; + } + private void writeRequestBodyAssertions(OperationShape operation, HttpRequestTestCase testCase) { testCase.getBody().ifPresent(body -> { // If we expect an empty body, expect it to be falsy. @@ -433,9 +449,50 @@ private void writeParamAssertions(Shape operationOrError, HttpResponseTestCase t .call(() -> params.accept(new CommandOutputNodeVisitor(testOutputShape))) .write("][0];"); + // Extract a payload binding if present. + Optional payloadBinding = operationOrError.asOperationShape() + .map(operationShape -> { + HttpBindingIndex index = HttpBindingIndex.of(model); + List payloadBindings = index.getResponseBindings(operationOrError, + Location.PAYLOAD); + if (!payloadBindings.isEmpty()) { + return payloadBindings.get(0); + } + return null; + }); + + // If we have a streaming payload blob, we need to collect it to something that + // can be compared with the test contents. This emulates the customer experience. + boolean hasStreamingPayloadBlob = payloadBinding + .map(binding -> + model.getShape(binding.getMember().getTarget()) + .filter(Shape::isBlobShape) + .filter(s -> s.hasTrait(StreamingTrait.ID)) + .isPresent()) + .orElse(false); + + // Do the collection for payload blobs. + if (hasStreamingPayloadBlob) { + writer.write("const comparableBlob = await client.config.streamCollector(r[$S]);", + payloadBinding.get().getMemberName()); + } + + // Perform parameter comparisons. writer.openBlock("Object.keys(paramsToValidate).forEach(param => {", "});", () -> { writer.write("expect(r[param]).toBeDefined();"); + if (hasStreamingPayloadBlob) { + writer.openBlock("if (param === $S) {", "} else {", payloadBinding.get().getMemberName(), () -> + writer.write("expect(equivalentContents(comparableBlob, " + + "paramsToValidate[param])).toBe(true);")); + writer.indent(); + } + writer.write("expect(equivalentContents(r[param], paramsToValidate[param])).toBe(true);"); + + if (hasStreamingPayloadBlob) { + writer.dedent(); + writer.write("}"); + } }); } } @@ -486,8 +543,7 @@ public Void booleanNode(BooleanNode node) { @Override public Void nullNode(NullNode node) { - // Handle nulls being literal "undefined" in JS. - writer.write("undefined,"); + writer.write("null,"); return null; } @@ -505,19 +561,30 @@ public Void numberNode(NumberNode node) { @Override public Void objectNode(ObjectNode node) { + // Short circuit document types, as the direct value is what we want. + if (workingShape.isDocumentShape()) { + writer.writeInline(Node.prettyPrintJson(node)); + return null; + } + // Both objects and maps can use a majority of the same logic. // Use "as any" to have TS complain less about undefined entries. writer.openBlock("{", "} as any,\n", () -> { Shape wrapperShape = this.workingShape; node.getMembers().forEach((keyNode, valueNode) -> { - writer.write("$L: ", keyNode.getValue()); + writer.writeInline("$L: ", keyNode.getValue()); // Grab the correct member related to the node member we have. MemberShape memberShape; if (wrapperShape.isStructureShape()) { memberShape = wrapperShape.asStructureShape().get().getMember(keyNode.getValue()).get(); - } else { + } else if (wrapperShape.isUnionShape()) { + memberShape = wrapperShape.asUnionShape().get().getMember(keyNode.getValue()).get(); + } else if (wrapperShape.isMapShape()) { memberShape = wrapperShape.asMapShape().get().getValue(); + } else { + throw new CodegenException("Unknown shape type for object node when " + + "generating protocol test input: " + wrapperShape.getType()); } // Handle auto-filling idempotency token values to the explicit value. @@ -631,6 +698,12 @@ public Void numberNode(NumberNode node) { @Override public Void objectNode(ObjectNode node) { + // Short circuit document types, as the direct value is what we want. + if (workingShape.isDocumentShape()) { + writer.writeInline(Node.prettyPrintJson(node)); + return null; + } + // Both objects and maps can use a majority of the same logic. // Use "as any" to have TS complain less about undefined entries. writer.openBlock("{", "},\n", () -> { @@ -640,8 +713,13 @@ public Void objectNode(ObjectNode node) { MemberShape memberShape; if (wrapperShape.isStructureShape()) { memberShape = wrapperShape.asStructureShape().get().getMember(keyNode.getValue()).get(); - } else { + } else if (wrapperShape.isUnionShape()) { + memberShape = wrapperShape.asUnionShape().get().getMember(keyNode.getValue()).get(); + } else if (wrapperShape.isMapShape()) { memberShape = wrapperShape.asMapShape().get().getValue(); + } else { + throw new CodegenException("Unknown shape type for object node when " + + "generating protocol test output: " + wrapperShape.getType()); } // Handle error standardization to the down-cased "message". 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 cd05de7f19b..33353cd6209 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 @@ -55,6 +55,7 @@ import software.amazon.smithy.model.traits.EventHeaderTrait; import software.amazon.smithy.model.traits.EventPayloadTrait; import software.amazon.smithy.model.traits.HttpTrait; +import software.amazon.smithy.model.traits.MediaTypeTrait; import software.amazon.smithy.model.traits.StreamingTrait; import software.amazon.smithy.model.traits.TimestampFormatTrait.Format; import software.amazon.smithy.typescript.codegen.ApplicationProtocol; @@ -153,8 +154,7 @@ public void generateSharedComponents(GenerationContext context) { * @return Returns if the shape is a native simple type. */ private boolean isNativeSimpleType(Shape target) { - return target instanceof BooleanShape || target instanceof DocumentShape - || target instanceof NumberShape || target instanceof StringShape; + return target instanceof BooleanShape || target instanceof NumberShape || target instanceof StringShape; } @Override @@ -216,9 +216,17 @@ private void generateOperationSerializer( writeHeaders(context, operation, bindingIndex); writeResolvedPath(context, operation, bindingIndex, trait); boolean hasQueryComponents = writeRequestQueryString(context, operation, bindingIndex, trait); + List bodyBindings = writeRequestBody(context, operation, bindingIndex); - boolean hasHostPrefix = operation.hasTrait(EndpointTrait.class); + if (!bodyBindings.isEmpty()) { + // Track all shapes bound to the body so their serializers may be generated. + bodyBindings.stream() + .map(HttpBinding::getMember) + .map(member -> context.getModel().expectShape(member.getTarget())) + .forEach(serializingDocumentShapes::add); + } + boolean hasHostPrefix = operation.hasTrait(EndpointTrait.class); if (hasHostPrefix) { HttpProtocolGeneratorUtils.writeHostPrefix(context, operation); } @@ -240,13 +248,6 @@ private void generateOperationSerializer( if (hasQueryComponents) { writer.write("query,"); } - if (!bodyBindings.isEmpty()) { - // Track all shapes bound to the body so their serializers may be generated. - bodyBindings.stream() - .map(HttpBinding::getMember) - .map(member -> context.getModel().expectShape(member.getTarget())) - .forEach(serializingDocumentShapes::add); - } // Always set the body, writer.write("body,"); }); @@ -351,8 +352,9 @@ private void writeHeaders( // Headers are always present either from the default document or the payload. writer.openBlock("const headers: any = {", "};", () -> { - writer.write("'Content-Type': $S,", bindingIndex.determineRequestContentType( - operation, getDocumentContentType())); + // Only set the content type if one can be determined. + bindingIndex.determineRequestContentType(operation, getDocumentContentType()).ifPresent(contentType -> + writer.write("'content-type': $S,", contentType)); writeDefaultHeaders(context, operation); operation.getInput().ifPresent(outputId -> { @@ -445,7 +447,7 @@ protected String getInputValue( Shape target ) { if (target instanceof StringShape) { - return HttpProtocolGeneratorUtils.getStringInputParam(context, target, dataSource); + return getStringInputParam(context, bindingType, dataSource, target); } else if (target instanceof FloatShape || target instanceof DoubleShape) { // Handle decimal numbers needing to have .0 in their value when whole numbers. return "((" + dataSource + " % 1 == 0) ? " + dataSource + " + \".0\" : " + dataSource + ".toString())"; @@ -453,6 +455,8 @@ protected String getInputValue( return dataSource + ".toString()"; } else if (target instanceof TimestampShape) { return getTimestampInputParam(context, bindingType, dataSource, member); + } else if (target instanceof DocumentShape) { + return dataSource; } else if (target instanceof BlobShape) { return getBlobInputParam(bindingType, dataSource); } else if (target instanceof CollectionShape) { @@ -466,6 +470,36 @@ protected String getInputValue( bindingType, member.getMemberName(), target.getType(), member.getContainer(), getName())); } + /** + * Given context and a source of data, generate an input value provider for the + * string. By default, this base64 encodes content in headers if there is a + * mediaType applied to the string, and passes through for all other cases. + * + * @param context The generation context. + * @param bindingType How this value is bound to the operation input. + * @param dataSource The in-code location of the data to provide an input of + * ({@code input.foo}, {@code entry}, etc.) + * @param target The shape of the value being provided. + * @return Returns a value or expression of the input string. + */ + private String getStringInputParam( + GenerationContext context, + Location bindingType, + String dataSource, + Shape target + ) { + String baseParam = HttpProtocolGeneratorUtils.getStringInputParam(context, target, dataSource); + switch (bindingType) { + case HEADER: + // Encode these to base64 if a MediaType is present. + if (target.hasTrait(MediaTypeTrait.ID)) { + return "Buffer.from(" + baseParam + ").toString('base64')"; + } + default: + return baseParam; + } + } + /** * Given context and a source of data, generate an input value provider for the * blob. By default, this base64 encodes content in headers and query strings, @@ -680,8 +714,21 @@ protected void serializeInputPayload( writer.openBlock("if (input.$L !== undefined) {", "}", memberName, () -> { Shape target = context.getModel().expectShape(payloadBinding.getMember().getTarget()); + + // Because documents can be set to a null value, handle setting that as the body + // instead of using toString, as `null.toString()` will fail. + if (target.isDocumentShape()) { + writer.openBlock("if (input.$L === null) {", "} else {", memberName, + () -> writer.write("body = \"null\";")); + writer.indent(); + + } writer.write("body = $L;", getInputValue( context, Location.PAYLOAD, "input." + memberName, payloadBinding.getMember(), target)); + if (target.isDocumentShape()) { + writer.dedent(); + writer.write("}"); + } }); } @@ -984,9 +1031,13 @@ private List readResponseBody( HttpBindingIndex bindingIndex ) { TypeScriptWriter writer = context.getWriter(); + SymbolProvider symbolProvider = context.getSymbolProvider(); + List documentBindings = bindingIndex.getResponseBindings(operationOrError, Location.DOCUMENT); documentBindings.sort(Comparator.comparing(HttpBinding::getMemberName)); List payloadBindings = bindingIndex.getResponseBindings(operationOrError, Location.PAYLOAD); + List responseCodeBindings = bindingIndex.getResponseBindings( + operationOrError, Location.RESPONSE_CODE); if (!documentBindings.isEmpty()) { // If the response has document bindings, the body can be parsed to a JavaScript object. @@ -998,25 +1049,55 @@ private List readResponseBody( writer.write("const data: any = $L;", bodyLocation); deserializeOutputDocument(context, operationOrError, documentBindings); - return documentBindings; } if (!payloadBindings.isEmpty()) { - return readResponsePayload(context, payloadBindings); + HttpBinding payloadBinding = readResponsePayload(context, payloadBindings.get(0)); + if (payloadBinding != null) { + return ListUtils.of(payloadBinding); + } else { + return ListUtils.of(); + } } - // If there are no payload or document bindings, the body still needs collected so the process can exit. + // Handle any potential httpResponseCode binding overrides if the field + // isn't set in the body. + // These are only relevant when a payload is not present, as it cannot + // coexist with a payload. + for (HttpBinding responseCodeBinding : responseCodeBindings) { + // The name of the member to get from the input shape. + String memberName = symbolProvider.toMemberName(responseCodeBinding.getMember()); + writer.openBlock("if (contents.$L === undefined) {", "}", memberName, () -> + writer.write("contents.$L = output.statusCode;", memberName)); + } + if (!documentBindings.isEmpty()) { + return documentBindings; + } + + // If there are no payload or document bindings, the body still needs to be + // collected so the process can exit. writer.write("await collectBody(output.body, context);"); return ListUtils.of(); } - private List readResponsePayload( + /** + * Writes the code needed to deserialized the output payload of a request. + * + *

Implementations of this method are expected to set a value to the + * bound member name of the {@code contents} variable after deserializing + * the response body. This variable will already be defined in scope. + * + * + * @param context The generation context. + * @param binding The payload binding to deserialize. + * @return The deserialized payload binding. + */ + protected HttpBinding readResponsePayload( GenerationContext context, - List payloadBindings + HttpBinding binding ) { TypeScriptWriter writer = context.getWriter(); // There can only be one payload binding. - HttpBinding binding = payloadBindings.get(0); Shape target = context.getModel().expectShape(binding.getMember().getTarget()); // Handle streaming shapes differently. @@ -1026,18 +1107,18 @@ private List readResponsePayload( generateEventStreamDeserializer(context, binding.getMember(), target); writer.write("contents.$L = data;", binding.getMemberName()); // Don't generate non-eventstream payload shape again. - return ListUtils.of(); + return null; } // If payload is streaming, return raw low-level stream directly. writer.write("const data: any = output.body;"); } else if (target instanceof BlobShape) { - // If payload is non-streaming blob, only need to collect stream to binary data(Uint8Array). + // If payload is non-streaming Blob, only need to collect stream to binary data (Uint8Array). writer.write("const data: any = await collectBody(output.body, context);"); } else if (target instanceof StructureShape || target instanceof UnionShape) { - // If body is Structure or Union, they we need to parse the string into JavaScript object. + // If payload is Structure or Union, they we need to parse the string into JavaScript object. writer.write("const data: any = await parseBody(output.body, context);"); - } else if (target instanceof StringShape) { - // If payload is string, we need to collect body and encode binary to string. + } else if (target instanceof StringShape || target instanceof DocumentShape) { + // If payload is String or Document, we need to collect body and convert binary to string. writer.write("const data: any = await collectBodyString(output.body, context);"); } else { throw new CodegenException(String.format("Unexpected shape type bound to payload: `%s`", @@ -1045,7 +1126,7 @@ private List readResponsePayload( } writer.write("contents.$L = $L;", binding.getMemberName(), getOutputValue(context, Location.PAYLOAD, "data", binding.getMember(), target)); - return payloadBindings; + return binding; } // Writes a function deserializing response payload to stream of event messages @@ -1264,7 +1345,7 @@ private String getOutputValue( } else if (target instanceof BooleanShape) { return getBooleanOutputParam(bindingType, dataSource); } else if (target instanceof StringShape) { - return HttpProtocolGeneratorUtils.getStringOutputParam(context, target, dataSource); + return getStringOutputParam(context, bindingType, dataSource, target); } else if (target instanceof DocumentShape) { return dataSource; } else if (target instanceof TimestampShape) { @@ -1303,6 +1384,32 @@ private String getBooleanOutputParam(Location bindingType, String dataSource) { } } + /** + * Given context and a source of data, generate an output value provider for the + * string. By default, this base64 decodes content in headers if there is a + * mediaType applied to the string, and passes through for all other cases. + * + * @param context The generation context. + * @param bindingType How this value is bound to the operation input. + * @param dataSource The in-code location of the data to provide an input of + * ({@code input.foo}, {@code entry}, etc.) + * @param target The shape of the value being provided. + * @return Returns a value or expression of the input string. + */ + private String getStringOutputParam( + GenerationContext context, + Location bindingType, + String dataSource, + Shape target + ) { + // Decode these to base64 if a MediaType is present. + if (bindingType == Location.HEADER && target.hasTrait(MediaTypeTrait.ID)) { + dataSource = "Buffer.from(" + dataSource + ", 'base64').toString('ascii')"; + } + + return HttpProtocolGeneratorUtils.getStringOutputParam(context, target, dataSource); + } + /** * Given context and a source of data, generate an output value provider for the * blob. By default, this base64 decodes content in headers and passes through 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 cf3f30899c9..cce8a38a9b1 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 @@ -195,7 +195,7 @@ private void writeRequestHeaders(GenerationContext context, OperationShape opera writer.addImport("HeaderBag", "__HeaderBag", "@aws-sdk/types"); writer.openBlock("const headers: __HeaderBag = {", "};", () -> { - writer.write("'Content-Type': $S,", getDocumentContentType()); + writer.write("'content-type': $S,", getDocumentContentType()); writeDefaultHeaders(context, operation); } ); 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 e89b062f0ef..e92a3137cb4 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 @@ -150,13 +150,8 @@ static String getSerFunctionName(Symbol symbol, String protocol) { // e.g., serializeAws_restJson1_1ExecuteStatement String functionName = "serialize" + ProtocolGenerator.getSanitizedName(protocol); - // These need intermediate serializers, so generate a separate name. - Shape shape = symbol.expectProperty("shape", Shape.class); - if (shape.isListShape() || shape.isSetShape() || shape.isMapShape()) { - functionName += shape.getId().getName(); - } else { - functionName += symbol.getName(); - } + // Update the function to have a component based on the symbol. + functionName += getSerdeFunctionSymbolComponent(symbol, symbol.expectProperty("shape", Shape.class)); return functionName; } @@ -172,17 +167,26 @@ static String getDeserFunctionName(Symbol symbol, String protocol) { // e.g., deserializeAws_restJson1_1ExecuteStatement String functionName = "deserialize" + ProtocolGenerator.getSanitizedName(protocol); - // These need intermediate serializers, so generate a separate name. - Shape shape = symbol.expectProperty("shape", Shape.class); - if (shape.isListShape() || shape.isSetShape() || shape.isMapShape()) { - functionName += shape.getId().getName(); - } else { - functionName += symbol.getName(); - } + // Update the function to have a component based on the symbol. + functionName += getSerdeFunctionSymbolComponent(symbol, symbol.expectProperty("shape", Shape.class)); return functionName; } + static String getSerdeFunctionSymbolComponent(Symbol symbol, Shape shape) { + switch (shape.getType()) { + case LIST: + case SET: + case MAP: + case DOCUMENT: + // These need specialized serializers because they use complex but + // non-generated types, so generate a separate name. + return shape.getId().getName(); + default: + return symbol.getName(); + } + } + /** * Context object used for service serialization and deserialization. */ diff --git a/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/integration/http-binding-utils.ts b/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/integration/http-binding-utils.ts index 2134a592adb..04245e7ddaf 100644 --- a/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/integration/http-binding-utils.ts +++ b/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/integration/http-binding-utils.ts @@ -1,5 +1,6 @@ const isSerializableHeaderValue = (value: any): boolean => value !== undefined && + value !== null && value !== "" && (!Object.getOwnPropertyNames(value).includes("length") || value.length != 0) &&