diff --git a/.changeset/small-gifts-tease.md b/.changeset/small-gifts-tease.md new file mode 100644 index 00000000000..f78e827d0bb --- /dev/null +++ b/.changeset/small-gifts-tease.md @@ -0,0 +1,5 @@ +--- +"@smithy/smithy-client": patch +--- + +serialize empty strings and collections in headers diff --git a/packages/smithy-client/src/index.ts b/packages/smithy-client/src/index.ts index 996345fec3e..ee08d54c3c5 100644 --- a/packages/smithy-client/src/index.ts +++ b/packages/smithy-client/src/index.ts @@ -1,4 +1,4 @@ -export * from "./NoOpLogger"; +export type { DocumentType, SdkError, SmithyException } from "@smithy/types"; export * from "./client"; export * from "./collect-stream-body"; export * from "./command"; @@ -8,16 +8,17 @@ export * from "./date-utils"; export * from "./default-error-handler"; export * from "./defaults-mode"; export * from "./emitWarningIfUnsupportedVersion"; -export * from "./extensions"; export * from "./exceptions"; export * from "./extended-encode-uri-component"; +export * from "./extensions"; export * from "./get-array-if-single-item"; export * from "./get-value-from-text-node"; +export * from "./is-serializable-header-value"; export * from "./lazy-json"; +export * from "./NoOpLogger"; export * from "./object-mapping"; export * from "./parse-utils"; export * from "./resolve-path"; export * from "./ser-utils"; export * from "./serde-json"; export * from "./split-every"; -export type { DocumentType, SdkError, SmithyException } from "@smithy/types"; diff --git a/packages/smithy-client/src/is-serializable-header-value.spec.ts b/packages/smithy-client/src/is-serializable-header-value.spec.ts new file mode 100644 index 00000000000..80370e4ad29 --- /dev/null +++ b/packages/smithy-client/src/is-serializable-header-value.spec.ts @@ -0,0 +1,23 @@ +import { isSerializableHeaderValue } from "./is-serializable-header-value"; + +describe(isSerializableHeaderValue.name, () => { + it("considers empty strings serializable", () => { + expect(isSerializableHeaderValue("")).toBe(true); + }); + + it("considers empty collections serializable", () => { + expect(isSerializableHeaderValue(new Set())).toBe(true); + expect(isSerializableHeaderValue([])).toBe(true); + }); + + it("considers most falsy data values to be serializable", () => { + expect(isSerializableHeaderValue(false)).toBe(true); + expect(isSerializableHeaderValue(0)).toBe(true); + expect(isSerializableHeaderValue(new Date(0))).toBe(true); + }); + + it("considered undefined and null to be unserializable", () => { + expect(isSerializableHeaderValue(undefined)).toBe(false); + expect(isSerializableHeaderValue(null)).toBe(false); + }); +}); diff --git a/packages/smithy-client/src/is-serializable-header-value.ts b/packages/smithy-client/src/is-serializable-header-value.ts new file mode 100644 index 00000000000..55b268222d5 --- /dev/null +++ b/packages/smithy-client/src/is-serializable-header-value.ts @@ -0,0 +1,7 @@ +/** + * @internal + * @returns whether the header value is serializable. + */ +export const isSerializableHeaderValue = (value: any) => { + return value != null; +}; 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 0f2761ab942..2df46ef0f47 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 @@ -201,7 +201,6 @@ public void generateSharedComponents(GenerationContext context) { generateDocumentBodyShapeDeserializers(context, deserializingDocumentShapes); HttpProtocolGeneratorUtils.generateMetadataDeserializer(context, getApplicationProtocol().getResponseType()); HttpProtocolGeneratorUtils.generateCollectBodyString(context); - HttpProtocolGeneratorUtils.generateHttpBindingUtils(context); writer.write( context.getStringStore().flushVariableDeclarationCode() @@ -965,6 +964,7 @@ private void writeRequestHeaders( opening = "const headers: any = {"; closing = "};"; } else { + writer.addImport("isSerializableHeaderValue", null, TypeScriptDependency.AWS_SMITHY_CLIENT); opening = normalHeaderCount > 0 ? "const headers: any = map({}, isSerializableHeaderValue, {" : "const headers: any = map({"; @@ -1035,6 +1035,8 @@ private void writeNormalHeader(GenerationContext context, HttpBinding binding) { : headerValue + defaultValue; // evaluated value has a function or method call attached + context.getWriter() + .addImport("isSerializableHeaderValue", null, TypeScriptDependency.AWS_SMITHY_CLIENT); headerBuffer.put(headerKey, String.format( "[%s]: [() => isSerializableHeaderValue(%s), () => %s],", context.getStringStore().var(headerKey), @@ -1093,6 +1095,7 @@ private void writeResponseHeaders( TypeScriptWriter writer = context.getWriter(); // Headers are always present either from the default document or the payload. + writer.addImport("isSerializableHeaderValue", null, TypeScriptDependency.AWS_SMITHY_CLIENT); writer.openBlock("let headers: any = map({}, isSerializableHeaderValue, {", "});", () -> { writeContentTypeHeader(context, operationOrError, false); injectExtraHeaders.run(); @@ -1373,7 +1376,13 @@ private String getCollectionInputParam( dataSource = "Array.from(" + dataSource + ".values())"; } String collectionTargetValue = getInputValue(context, bindingType, "_entry", targetMember, collectionTarget); - String iteratedParam = "(" + dataSource + " || []).map(_entry => " + collectionTargetValue + " as any)"; + String iteratedParam; + if (collectionTargetValue.equals("_entry")) { + iteratedParam = "(" + dataSource + " || [])"; + } else { + iteratedParam = "(" + dataSource + " || []).map(_entry => " + collectionTargetValue + " as any)"; + } + switch (bindingType) { case HEADER: return iteratedParam + ".join(', ')"; @@ -2689,6 +2698,9 @@ private String getCollectionOutputParam( switch (bindingType) { case QUERY_PARAMS: case QUERY: + if (collectionTargetValue.equals("_entry")) { + return String.format("%1$s", dataSource); + } return String.format("%1$s.map(_entry => %2$s as any)", dataSource, collectionTargetValue); case LABEL: dataSource = "(" + dataSource + " || \"\")"; @@ -2696,13 +2708,15 @@ private String getCollectionOutputParam( outputParam = "" + dataSource + ".split('/')"; // Iterate over each entry and do deser work. - outputParam += ".map(_entry => " + collectionTargetValue + " as any)"; + if (!collectionTargetValue.equals("_entry")) { + outputParam += ".map(_entry => " + collectionTargetValue + " as any)"; + } return outputParam; case HEADER: dataSource = "(" + dataSource + " || \"\")"; // Split these values on commas. - outputParam = "" + dataSource + ".split(',')"; + outputParam = dataSource + ".split(',')"; // Headers that have HTTP_DATE formatted timestamps already contain a "," // in their formatted entry, so split on every other "," instead. @@ -2719,7 +2733,9 @@ private String getCollectionOutputParam( } // Iterate over each entry and do deser work. - outputParam += ".map(_entry => " + collectionTargetValue + " as any)"; + if (!collectionTargetValue.equals("_entry")) { + outputParam += ".map(_entry => " + collectionTargetValue + " as any)"; + } return outputParam; default: 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 55a7fc6905a..849cc03d80f 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 @@ -47,7 +47,6 @@ import software.amazon.smithy.typescript.codegen.TypeScriptDependency; import software.amazon.smithy.typescript.codegen.TypeScriptWriter; import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext; -import software.amazon.smithy.utils.IoUtils; import software.amazon.smithy.utils.SmithyInternalApi; import software.amazon.smithy.utils.SmithyUnstableApi; @@ -279,16 +278,6 @@ public static void generateCollectBodyString(GenerationContext context) { writer.write(""); } - /** - * Writes any additional utils needed for HTTP protocols with bindings. - * - * @param context The generation context. - */ - static void generateHttpBindingUtils(GenerationContext context) { - TypeScriptWriter writer = context.getWriter(); - writer.write(IoUtils.readUtf8Resource(HttpProtocolGeneratorUtils.class, "http-binding-utils.ts")); - } - /** * Writes $retryable key for error if it contains RetryableTrait. * 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 deleted file mode 100644 index 04245e7ddaf..00000000000 --- a/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/integration/http-binding-utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -const isSerializableHeaderValue = (value: any): boolean => - value !== undefined && - value !== null && - value !== "" && - (!Object.getOwnPropertyNames(value).includes("length") || - value.length != 0) && - (!Object.getOwnPropertyNames(value).includes("size") || value.size != 0);