Skip to content

Commit d757a15

Browse files
committed
handle unset unions
1 parent 38d7253 commit d757a15

File tree

8 files changed

+122
-5
lines changed

8 files changed

+122
-5
lines changed

.changeset/witty-rings-do.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/smithy-client": minor
3+
---
4+
5+
add quoteHeader function

packages/smithy-client/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export * from "./lazy-json";
1818
export * from "./NoOpLogger";
1919
export * from "./object-mapping";
2020
export * from "./parse-utils";
21+
export * from "./quote-header";
2122
export * from "./resolve-path";
2223
export * from "./ser-utils";
2324
export * from "./serde-json";
2425
export * from "./split-every";
26+
export * from "./split-header";
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { quoteHeader } from "./quote-header";
2+
3+
describe(quoteHeader.name, () => {
4+
it("should not wrap header elements that don't include the delimiter or double quotes", () => {
5+
expect(quoteHeader("bc")).toBe("bc");
6+
});
7+
8+
it("should wrap header elements that include the delimiter", () => {
9+
expect(quoteHeader("b,c")).toBe('"b,c"');
10+
});
11+
12+
it("should wrap header elements that include double quotes", () => {
13+
expect(quoteHeader(`"bc"`)).toBe('"\\"bc\\""');
14+
});
15+
16+
it("should wrap header elements that include the delimiter and double quotes", () => {
17+
expect(quoteHeader(`"b,c"`)).toBe('"\\"b,c\\""');
18+
});
19+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @public
3+
* @param part - header list element
4+
* @returns quoted string if part contains delimiter.
5+
*/
6+
export function quoteHeader(part: string) {
7+
if (part.includes(",") || part.includes('"')) {
8+
part = `"${part.replace(/"/g, '\\"')}"`;
9+
}
10+
return part;
11+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { splitHeader } from "./split-header";
2+
3+
describe(splitHeader.name, () => {
4+
it("should split a string by commas and trim only the comma delimited outer values", () => {
5+
expect(splitHeader("abc")).toEqual(["abc"]);
6+
expect(splitHeader("a,b,c")).toEqual(["a", "b", "c"]);
7+
expect(splitHeader("a, b, c")).toEqual(["a", "b", "c"]);
8+
expect(splitHeader("a , b , c")).toEqual(["a", "b", "c"]);
9+
expect(splitHeader(`a , b , " c "`)).toEqual(["a", "b", " c "]);
10+
});
11+
it("should split a string by commas that are not in quotes, and remove outer quotes", () => {
12+
expect(splitHeader('"b,c", "\\"def\\"", a')).toEqual(["b,c", '"def"', "a"]);
13+
expect(splitHeader('"a,b,c", ""def"", "a,b ,c"')).toEqual(["a,b,c", '"def"', "a,b ,c"]);
14+
});
15+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @param value - header string value.
3+
* @returns value split by commas that aren't in quotes.
4+
*/
5+
export const splitHeader = (value: string): string[] => {
6+
const z = value.length;
7+
const values = [];
8+
9+
let withinQuotes = false;
10+
let prevChar = undefined;
11+
let anchor = 0;
12+
13+
for (let i = 0; i < z; ++i) {
14+
const char = value[i];
15+
switch (char) {
16+
case `"`:
17+
if (prevChar !== "\\") {
18+
withinQuotes = !withinQuotes;
19+
}
20+
break;
21+
case ",":
22+
if (!withinQuotes) {
23+
values.push(value.slice(anchor, i));
24+
anchor = i + 1;
25+
}
26+
break;
27+
default:
28+
}
29+
prevChar = char;
30+
}
31+
32+
values.push(value.slice(anchor));
33+
34+
return values.map((v) => {
35+
v = v.trim();
36+
const z = v.length;
37+
if (v[0] === `"` && v[z - 1] === `"`) {
38+
v = v.slice(1, z - 1);
39+
}
40+
return v.replace(/\\"/g, '"');
41+
});
42+
};

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/HttpProtocolTestGenerator.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ public final class HttpProtocolTestGenerator implements Runnable {
9595
private static final Set<String> IGNORE_COMMA_SPACING = SetUtils.of(
9696
"content-encoding"
9797
);
98+
private static final CharSequence TWO_BACKSLASHES = "\\\\";
99+
private static final CharSequence ONE_BACKSLASH = "\\";
98100

99101
private final TypeScriptSettings settings;
100102
private final Model model;
@@ -545,9 +547,11 @@ private void writeHttpHostAssertion(HttpRequestTestCase testCase) {
545547
}
546548

547549
private void writeHttpBodyAssertions(String body, String mediaType, boolean isClientTest) {
548-
// If we expect an empty body, expect it to be falsy.
549550
if (body.isEmpty()) {
550-
writer.write("expect(r.body).toBeFalsy();");
551+
// If we expect an empty body, expect it to be falsy.
552+
// Or, for JSON an empty object represents an empty body.
553+
// mediaType is often UNKNOWN here.
554+
writer.write("expect(!r.body || r.body === `{}`).toBeTruthy();");
551555
return;
552556
}
553557

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,6 +1385,11 @@ private String getCollectionInputParam(
13851385

13861386
switch (bindingType) {
13871387
case HEADER:
1388+
if (collectionTarget.isStringShape()) {
1389+
context.getWriter().addImport(
1390+
"quoteHeader", "__quoteHeader", TypeScriptDependency.AWS_SMITHY_CLIENT);
1391+
return iteratedParam + ".map(__quoteHeader).join(', ')";
1392+
}
13881393
return iteratedParam + ".join(', ')";
13891394
case QUERY:
13901395
case QUERY_PARAMS:
@@ -2466,16 +2471,29 @@ private HttpBinding readPayload(
24662471
// If payload is a Union, then we need to parse the string into JavaScript object.
24672472
importUnionDeserializer(writer);
24682473
writer.write("const data: Record<string, any> | undefined "
2469-
+ "= __expectUnion(await parseBody(output.body, context));");
2474+
+ "= await parseBody(output.body, context);");
24702475
} else if (target instanceof StringShape || target instanceof DocumentShape) {
24712476
// If payload is String or Document, we need to collect body and convert binary to string.
24722477
writer.write("const data: any = await collectBodyString(output.body, context);");
24732478
} else {
24742479
throw new CodegenException(String.format("Unexpected shape type bound to payload: `%s`",
24752480
target.getType()));
24762481
}
2477-
writer.write("contents.$L = $L;", binding.getMemberName(), getOutputValue(context,
2482+
2483+
if (target instanceof UnionShape) {
2484+
writer.openBlock(
2485+
"if (Object.keys(data ?? {}).length) {",
2486+
"}",
2487+
() -> {
2488+
writer.write("contents.$L = __expectUnion($L);", binding.getMemberName(), getOutputValue(context,
2489+
Location.PAYLOAD, "data", binding.getMember(), target));
2490+
}
2491+
);
2492+
} else {
2493+
writer.write("contents.$L = $L;", binding.getMemberName(), getOutputValue(context,
24782494
Location.PAYLOAD, "data", binding.getMember(), target));
2495+
}
2496+
24792497
return binding;
24802498
}
24812499

@@ -2716,7 +2734,8 @@ private String getCollectionOutputParam(
27162734
case HEADER:
27172735
dataSource = "(" + dataSource + " || \"\")";
27182736
// Split these values on commas.
2719-
outputParam = dataSource + ".split(',')";
2737+
context.getWriter().addImport("splitHeader", "__splitHeader", TypeScriptDependency.AWS_SMITHY_CLIENT);
2738+
outputParam = "__splitHeader(" + dataSource + ")";
27202739

27212740
// Headers that have HTTP_DATE formatted timestamps already contain a ","
27222741
// in their formatted entry, so split on every other "," instead.

0 commit comments

Comments
 (0)