Skip to content

Commit 35c2bf2

Browse files
authored
chore(core/protocols): generate idempotencyTokens in ShapeSerializers (#7247)
1 parent 7312c1b commit 35c2bf2

File tree

5 files changed

+99
-19
lines changed

5 files changed

+99
-19
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { CborShapeSerializer } from "@smithy/core/cbor";
2+
import { sim, struct } from "@smithy/core/schema";
3+
import { describe, expect, test as it } from "vitest";
4+
5+
import { JsonShapeSerializer } from "./json/JsonShapeSerializer";
6+
import { QueryShapeSerializer } from "./query/QueryShapeSerializer";
7+
import { XmlShapeSerializer } from "./xml/XmlShapeSerializer";
8+
9+
describe("idempotencyToken", () => {
10+
const structureSchema = struct(
11+
"ns",
12+
"StructureWithIdempotencyToken",
13+
0,
14+
["idempotencyToken", "plain"],
15+
[sim("ns", "IdempotencyTokenString", 0, 0b0100), sim("ns", "PlainString", 0, 0b0000)]
16+
);
17+
18+
it("all ShapeSerializer implementations should generate an idempotency token if no input was provided by the caller", () => {
19+
const serializers = [
20+
new JsonShapeSerializer({
21+
timestampFormat: { default: 7, useTrait: true },
22+
jsonName: true,
23+
}),
24+
new QueryShapeSerializer({
25+
timestampFormat: { default: 7, useTrait: true },
26+
}),
27+
new XmlShapeSerializer({
28+
serviceNamespace: "ServiceNamespace",
29+
timestampFormat: { default: 7, useTrait: true },
30+
xmlNamespace: "XmlNamespace",
31+
}),
32+
new CborShapeSerializer(),
33+
];
34+
35+
const expectedSerializations = [
36+
/{"idempotencyToken":"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}","plain":"potatoes"}/,
37+
/&idempotencyToken=[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}&plain=potatoes/,
38+
/<StructureWithIdempotencyToken xmlns="XmlNamespace"><idempotencyToken>([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})<\/idempotencyToken><plain>potatoes<\/plain><\/StructureWithIdempotencyToken>/,
39+
/pidempotencyTokenx\$([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})eplainhpotatoes/,
40+
];
41+
42+
for (let i = 0; i < expectedSerializations.length; i++) {
43+
const serializer = serializers[i];
44+
const expectedSerialization = expectedSerializations[i];
45+
46+
// automatic token
47+
{
48+
serializer.write(structureSchema, {
49+
idempotencyToken: undefined,
50+
plain: "potatoes",
51+
});
52+
const data = serializer.flush();
53+
const serialization = Buffer.from(data).toString("utf8");
54+
expect(serialization).toMatch(expectedSerialization);
55+
}
56+
57+
// manual token
58+
{
59+
serializer.write(structureSchema, {
60+
idempotencyToken: "00000000-0000-4000-9000-000000000000",
61+
plain: "potatoes",
62+
});
63+
const data = serializer.flush();
64+
const serialization = Buffer.from(data).toString("utf8");
65+
expect(serialization).toMatch(expectedSerialization);
66+
}
67+
}
68+
});
69+
});

packages/core/src/submodules/protocols/json/JsonShapeSerializer.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { NormalizedSchema, SCHEMA } from "@smithy/core/schema";
2-
import { dateToUtcString } from "@smithy/core/serde";
3-
import { LazyJsonString } from "@smithy/core/serde";
2+
import { dateToUtcString, generateIdempotencyToken, LazyJsonString } from "@smithy/core/serde";
43
import { Schema, ShapeSerializer } from "@smithy/types";
54

65
import { SerdeContextConfig } from "../ConfigurableSerdeContext";
@@ -110,11 +109,18 @@ export class JsonShapeSerializer extends SerdeContextConfig implements ShapeSeri
110109
}
111110
}
112111

113-
const mediaType = ns.getMergedTraits().mediaType;
114-
if (ns.isStringSchema() && typeof value === "string" && mediaType) {
115-
const isJson = mediaType === "application/json" || mediaType.endsWith("+json");
116-
if (isJson) {
117-
return LazyJsonString.from(value);
112+
if (ns.isStringSchema()) {
113+
if (typeof value === "undefined" && ns.isIdempotencyToken()) {
114+
return generateIdempotencyToken();
115+
}
116+
117+
const mediaType = ns.getMergedTraits().mediaType;
118+
119+
if (typeof value === "string" && mediaType) {
120+
const isJson = mediaType === "application/json" || mediaType.endsWith("+json");
121+
if (isJson) {
122+
return LazyJsonString.from(value);
123+
}
118124
}
119125
}
120126

packages/core/src/submodules/protocols/json/jsonReplacer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class JsonReplacer {
3333

3434
return (key: string, value: unknown) => {
3535
if (value instanceof NumericValue) {
36-
const v = `${NUMERIC_CONTROL_CHAR + +"nv" + this.counter++}_` + value.string;
36+
const v = `${NUMERIC_CONTROL_CHAR + "nv" + this.counter++}_` + value.string;
3737
this.values.set(`"${v}"`, value.string);
3838
return v;
3939
}

packages/core/src/submodules/protocols/query/QueryShapeSerializer.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { determineTimestampFormat, extendedEncodeURIComponent } from "@smithy/core/protocols";
22
import { NormalizedSchema, SCHEMA } from "@smithy/core/schema";
3-
import { NumericValue } from "@smithy/core/serde";
3+
import { generateIdempotencyToken, NumericValue } from "@smithy/core/serde";
44
import { dateToUtcString } from "@smithy/smithy-client";
55
import type { Schema, ShapeSerializer } from "@smithy/types";
66
import { toBase64 } from "@smithy/util-base64";
@@ -36,6 +36,9 @@ export class QueryShapeSerializer extends SerdeContextConfig implements ShapeSer
3636
if (value != null) {
3737
this.writeKey(prefix);
3838
this.writeValue(String(value));
39+
} else if (ns.isIdempotencyToken()) {
40+
this.writeKey(prefix);
41+
this.writeValue(generateIdempotencyToken());
3942
}
4043
} else if (ns.isBigIntegerSchema()) {
4144
if (value != null) {
@@ -111,7 +114,7 @@ export class QueryShapeSerializer extends SerdeContextConfig implements ShapeSer
111114
} else if (ns.isStructSchema()) {
112115
if (value && typeof value === "object") {
113116
for (const [memberName, member] of ns.structIterator()) {
114-
if ((value as any)[memberName] == null) {
117+
if ((value as any)[memberName] == null && !member.isIdempotencyToken()) {
115118
continue;
116119
}
117120
const suffix = this.getKey(memberName, member.getMergedTraits().xmlName);

packages/core/src/submodules/protocols/xml/XmlShapeSerializer.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { XmlNode, XmlText } from "@aws-sdk/xml-builder";
22
import { NormalizedSchema, SCHEMA } from "@smithy/core/schema";
3-
import { NumericValue } from "@smithy/core/serde";
3+
import { generateIdempotencyToken, NumericValue } from "@smithy/core/serde";
44
import { dateToUtcString } from "@smithy/smithy-client";
55
import type { Schema as ISchema, ShapeSerializer } from "@smithy/types";
66
import { fromBase64, toBase64 } from "@smithy/util-base64";
@@ -86,7 +86,7 @@ export class XmlShapeSerializer extends SerdeContextConfig implements ShapeSeria
8686
for (const [memberName, memberSchema] of ns.structIterator()) {
8787
const val = (value as any)[memberName];
8888

89-
if (val != null) {
89+
if (val != null || memberSchema.isIdempotencyToken()) {
9090
if (memberSchema.getMergedTraits().xmlAttribute) {
9191
structXmlNode.addAttribute(
9292
memberSchema.getMergedTraits().xmlName ?? memberName,
@@ -298,16 +298,18 @@ export class XmlShapeSerializer extends SerdeContextConfig implements ShapeSeria
298298
}
299299
}
300300

301-
if (
302-
ns.isStringSchema() ||
303-
ns.isBooleanSchema() ||
304-
ns.isNumericSchema() ||
305-
ns.isBigIntegerSchema() ||
306-
ns.isBigDecimalSchema()
307-
) {
301+
if (ns.isBooleanSchema() || ns.isNumericSchema() || ns.isBigIntegerSchema() || ns.isBigDecimalSchema()) {
308302
nodeContents = String(value);
309303
}
310304

305+
if (ns.isStringSchema()) {
306+
if (value === undefined && ns.isIdempotencyToken()) {
307+
nodeContents = generateIdempotencyToken();
308+
} else {
309+
nodeContents = String(value);
310+
}
311+
}
312+
311313
if (nodeContents === null) {
312314
throw new Error(`Unhandled schema-value pair ${ns.getName(true)}=${value}`);
313315
}

0 commit comments

Comments
 (0)