From d757a1518a384a860e379c2bf294432d71bbdca4 Mon Sep 17 00:00:00 2001 From: George Fu Date: Wed, 25 Sep 2024 19:08:50 +0000 Subject: [PATCH] handle unset unions --- .changeset/witty-rings-do.md | 5 +++ packages/smithy-client/src/index.ts | 2 + .../smithy-client/src/quote-header.spec.ts | 19 +++++++++ packages/smithy-client/src/quote-header.ts | 11 +++++ .../smithy-client/src/split-header.spec.ts | 15 +++++++ packages/smithy-client/src/split-header.ts | 42 +++++++++++++++++++ .../codegen/HttpProtocolTestGenerator.java | 8 +++- .../HttpBindingProtocolGenerator.java | 25 +++++++++-- 8 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 .changeset/witty-rings-do.md create mode 100644 packages/smithy-client/src/quote-header.spec.ts create mode 100644 packages/smithy-client/src/quote-header.ts create mode 100644 packages/smithy-client/src/split-header.spec.ts create mode 100644 packages/smithy-client/src/split-header.ts diff --git a/.changeset/witty-rings-do.md b/.changeset/witty-rings-do.md new file mode 100644 index 00000000000..e56f9652bce --- /dev/null +++ b/.changeset/witty-rings-do.md @@ -0,0 +1,5 @@ +--- +"@smithy/smithy-client": minor +--- + +add quoteHeader function diff --git a/packages/smithy-client/src/index.ts b/packages/smithy-client/src/index.ts index ee08d54c3c5..4a4ac197236 100644 --- a/packages/smithy-client/src/index.ts +++ b/packages/smithy-client/src/index.ts @@ -18,7 +18,9 @@ export * from "./lazy-json"; export * from "./NoOpLogger"; export * from "./object-mapping"; export * from "./parse-utils"; +export * from "./quote-header"; export * from "./resolve-path"; export * from "./ser-utils"; export * from "./serde-json"; export * from "./split-every"; +export * from "./split-header"; diff --git a/packages/smithy-client/src/quote-header.spec.ts b/packages/smithy-client/src/quote-header.spec.ts new file mode 100644 index 00000000000..3609f96c22f --- /dev/null +++ b/packages/smithy-client/src/quote-header.spec.ts @@ -0,0 +1,19 @@ +import { quoteHeader } from "./quote-header"; + +describe(quoteHeader.name, () => { + it("should not wrap header elements that don't include the delimiter or double quotes", () => { + expect(quoteHeader("bc")).toBe("bc"); + }); + + it("should wrap header elements that include the delimiter", () => { + expect(quoteHeader("b,c")).toBe('"b,c"'); + }); + + it("should wrap header elements that include double quotes", () => { + expect(quoteHeader(`"bc"`)).toBe('"\\"bc\\""'); + }); + + it("should wrap header elements that include the delimiter and double quotes", () => { + expect(quoteHeader(`"b,c"`)).toBe('"\\"b,c\\""'); + }); +}); diff --git a/packages/smithy-client/src/quote-header.ts b/packages/smithy-client/src/quote-header.ts new file mode 100644 index 00000000000..d7eb7d60349 --- /dev/null +++ b/packages/smithy-client/src/quote-header.ts @@ -0,0 +1,11 @@ +/** + * @public + * @param part - header list element + * @returns quoted string if part contains delimiter. + */ +export function quoteHeader(part: string) { + if (part.includes(",") || part.includes('"')) { + part = `"${part.replace(/"/g, '\\"')}"`; + } + return part; +} diff --git a/packages/smithy-client/src/split-header.spec.ts b/packages/smithy-client/src/split-header.spec.ts new file mode 100644 index 00000000000..fd6933fc5fc --- /dev/null +++ b/packages/smithy-client/src/split-header.spec.ts @@ -0,0 +1,15 @@ +import { splitHeader } from "./split-header"; + +describe(splitHeader.name, () => { + it("should split a string by commas and trim only the comma delimited outer values", () => { + expect(splitHeader("abc")).toEqual(["abc"]); + expect(splitHeader("a,b,c")).toEqual(["a", "b", "c"]); + expect(splitHeader("a, b, c")).toEqual(["a", "b", "c"]); + expect(splitHeader("a , b , c")).toEqual(["a", "b", "c"]); + expect(splitHeader(`a , b , " c "`)).toEqual(["a", "b", " c "]); + }); + it("should split a string by commas that are not in quotes, and remove outer quotes", () => { + expect(splitHeader('"b,c", "\\"def\\"", a')).toEqual(["b,c", '"def"', "a"]); + expect(splitHeader('"a,b,c", ""def"", "a,b ,c"')).toEqual(["a,b,c", '"def"', "a,b ,c"]); + }); +}); diff --git a/packages/smithy-client/src/split-header.ts b/packages/smithy-client/src/split-header.ts new file mode 100644 index 00000000000..da61b573b59 --- /dev/null +++ b/packages/smithy-client/src/split-header.ts @@ -0,0 +1,42 @@ +/** + * @param value - header string value. + * @returns value split by commas that aren't in quotes. + */ +export const splitHeader = (value: string): string[] => { + const z = value.length; + const values = []; + + let withinQuotes = false; + let prevChar = undefined; + let anchor = 0; + + for (let i = 0; i < z; ++i) { + const char = value[i]; + switch (char) { + case `"`: + if (prevChar !== "\\") { + withinQuotes = !withinQuotes; + } + break; + case ",": + if (!withinQuotes) { + values.push(value.slice(anchor, i)); + anchor = i + 1; + } + break; + default: + } + prevChar = char; + } + + values.push(value.slice(anchor)); + + return values.map((v) => { + v = v.trim(); + const z = v.length; + if (v[0] === `"` && v[z - 1] === `"`) { + v = v.slice(1, z - 1); + } + return v.replace(/\\"/g, '"'); + }); +}; 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 9154108edcd..be72489c8c4 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 @@ -95,6 +95,8 @@ public final class HttpProtocolTestGenerator implements Runnable { private static final Set IGNORE_COMMA_SPACING = SetUtils.of( "content-encoding" ); + private static final CharSequence TWO_BACKSLASHES = "\\\\"; + private static final CharSequence ONE_BACKSLASH = "\\"; private final TypeScriptSettings settings; private final Model model; @@ -545,9 +547,11 @@ private void writeHttpHostAssertion(HttpRequestTestCase testCase) { } private void writeHttpBodyAssertions(String body, String mediaType, boolean isClientTest) { - // If we expect an empty body, expect it to be falsy. if (body.isEmpty()) { - writer.write("expect(r.body).toBeFalsy();"); + // If we expect an empty body, expect it to be falsy. + // Or, for JSON an empty object represents an empty body. + // mediaType is often UNKNOWN here. + writer.write("expect(!r.body || r.body === `{}`).toBeTruthy();"); return; } 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 2df46ef0f47..3341f9d7ab4 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 @@ -1385,6 +1385,11 @@ private String getCollectionInputParam( switch (bindingType) { case HEADER: + if (collectionTarget.isStringShape()) { + context.getWriter().addImport( + "quoteHeader", "__quoteHeader", TypeScriptDependency.AWS_SMITHY_CLIENT); + return iteratedParam + ".map(__quoteHeader).join(', ')"; + } return iteratedParam + ".join(', ')"; case QUERY: case QUERY_PARAMS: @@ -2466,7 +2471,7 @@ private HttpBinding readPayload( // If payload is a Union, then we need to parse the string into JavaScript object. importUnionDeserializer(writer); writer.write("const data: Record | undefined " - + "= __expectUnion(await parseBody(output.body, context));"); + + "= await parseBody(output.body, context);"); } 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);"); @@ -2474,8 +2479,21 @@ private HttpBinding readPayload( throw new CodegenException(String.format("Unexpected shape type bound to payload: `%s`", target.getType())); } - writer.write("contents.$L = $L;", binding.getMemberName(), getOutputValue(context, + + if (target instanceof UnionShape) { + writer.openBlock( + "if (Object.keys(data ?? {}).length) {", + "}", + () -> { + writer.write("contents.$L = __expectUnion($L);", binding.getMemberName(), getOutputValue(context, + Location.PAYLOAD, "data", binding.getMember(), target)); + } + ); + } else { + writer.write("contents.$L = $L;", binding.getMemberName(), getOutputValue(context, Location.PAYLOAD, "data", binding.getMember(), target)); + } + return binding; } @@ -2716,7 +2734,8 @@ private String getCollectionOutputParam( case HEADER: dataSource = "(" + dataSource + " || \"\")"; // Split these values on commas. - outputParam = dataSource + ".split(',')"; + context.getWriter().addImport("splitHeader", "__splitHeader", TypeScriptDependency.AWS_SMITHY_CLIENT); + outputParam = "__splitHeader(" + dataSource + ")"; // Headers that have HTTP_DATE formatted timestamps already contain a "," // in their formatted entry, so split on every other "," instead.