Skip to content

Commit ccca2be

Browse files
committed
chore(core/cbor): idempotencyToken handling for CborCodec
1 parent 64e033f commit ccca2be

File tree

12 files changed

+146
-8
lines changed

12 files changed

+146
-8
lines changed

.changeset/gentle-dogs-protect.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/middleware-retry": patch
3+
---
4+
5+
update uuid types version

.changeset/quiet-yaks-bathe.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/core": minor
3+
---
4+
5+
handle idempotency token generation for CBOR protocol

packages/core/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@
7878
"@smithy/util-middleware": "workspace:^",
7979
"@smithy/util-stream": "workspace:^",
8080
"@smithy/util-utf8": "workspace:^",
81-
"tslib": "^2.6.2"
81+
"@types/uuid": "^9.0.1",
82+
"tslib": "^2.6.2",
83+
"uuid": "^9.0.1"
8284
},
8385
"engines": {
8486
"node": ">=18.0.0"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { NormalizedSchema, sim, struct } from "@smithy/core/schema";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { cbor } from "./cbor";
5+
import { CborCodec, CborShapeSerializer } from "./CborCodec";
6+
7+
describe(CborShapeSerializer.name, () => {
8+
const codec = new CborCodec();
9+
10+
const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
11+
12+
const idempotencyTokenSchemas = [
13+
NormalizedSchema.of(sim("", "StringWithTraits", 0, 0b0100)),
14+
NormalizedSchema.of(sim("", "StringWithTraits", 0, { idempotencyToken: 1 })),
15+
];
16+
17+
const plainSchemas = [
18+
NormalizedSchema.of(0),
19+
NormalizedSchema.of(sim("", "StringWithTraits", 0, 0)),
20+
NormalizedSchema.of(sim("", "StringWithTraits", 0, {})),
21+
];
22+
23+
const serializer = codec.createSerializer();
24+
25+
describe("serialization", () => {
26+
it("should generate an idempotency token when the input for such a member is undefined", () => {
27+
for (const idempotencyTokenSchema of idempotencyTokenSchemas) {
28+
for (const plainSchema of plainSchemas) {
29+
const objectSchema = struct(
30+
"ns",
31+
"StructWithIdempotencyToken",
32+
0,
33+
["idempotencyToken", "plainString"],
34+
[idempotencyTokenSchema, plainSchema]
35+
);
36+
37+
serializer.write(objectSchema, {
38+
idempotencyToken: undefined,
39+
plainString: undefined,
40+
});
41+
expect(cbor.deserialize(serializer.flush())).toMatchObject({
42+
idempotencyToken: UUID_V4,
43+
});
44+
45+
serializer.write(objectSchema, {
46+
idempotencyToken: undefined,
47+
plainString: "abc",
48+
});
49+
expect(cbor.deserialize(serializer.flush())).toMatchObject({
50+
idempotencyToken: UUID_V4,
51+
plainString: /^abc$/,
52+
});
53+
}
54+
}
55+
});
56+
});
57+
});

packages/core/src/submodules/cbor/CborCodec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NormalizedSchema } from "@smithy/core/schema";
2-
import { parseEpochTimestamp } from "@smithy/core/serde";
2+
import { generateIdempotencyToken, parseEpochTimestamp } from "@smithy/core/serde";
33
import { Codec, Schema, SerdeFunctions, ShapeDeserializer, ShapeSerializer } from "@smithy/types";
44

55
import { cbor } from "./cbor";
@@ -52,10 +52,14 @@ export class CborShapeSerializer implements ShapeSerializer {
5252

5353
switch (typeof source) {
5454
case "undefined":
55+
if (ns.isIdempotencyToken()) {
56+
return generateIdempotencyToken();
57+
}
5558
return null;
5659
case "boolean":
5760
case "number":
5861
case "string":
62+
return source;
5963
case "bigint":
6064
case "symbol":
6165
return source;

packages/core/src/submodules/schema/schemas/NormalizedSchema.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,29 @@ describe(NormalizedSchema.name, () => {
214214
});
215215
});
216216
});
217+
218+
describe("idempotency token detection", () => {
219+
const idempotencyTokenSchemas = [
220+
NormalizedSchema.of(sim("", "StringWithTraits", 0, 0b0100)),
221+
NormalizedSchema.of(sim("", "StringWithTraits", 0, { idempotencyToken: 1 })),
222+
];
223+
224+
const plainSchemas = [
225+
NormalizedSchema.of(0),
226+
NormalizedSchema.of(sim("", "StringWithTraits", 0, 0)),
227+
NormalizedSchema.of(sim("", "StringWithTraits", 0, {})),
228+
];
229+
230+
it("has a consistent shortcut method for idempotencyToken detection", () => {
231+
for (const schema of idempotencyTokenSchemas) {
232+
expect(schema.isIdempotencyToken()).toBe(true);
233+
expect(schema.getMergedTraits().idempotencyToken).toBe(1);
234+
}
235+
236+
for (const schema of plainSchemas) {
237+
expect(schema.isIdempotencyToken()).toBe(false);
238+
expect(schema.getMergedTraits().idempotencyToken).toBe(undefined);
239+
}
240+
});
241+
});
217242
});

packages/core/src/submodules/schema/schemas/NormalizedSchema.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,19 @@ export class NormalizedSchema implements INormalizedSchema {
273273
return this.getSchema() === SCHEMA.STREAMING_BLOB;
274274
}
275275

276+
/**
277+
* This is a shortcut to avoid calling `getMergedTraits().idempotencyToken` on every string.
278+
* @returns whether the schema has the idempotencyToken trait.
279+
*/
280+
public isIdempotencyToken(): boolean {
281+
if (typeof this.traits === "number") {
282+
return (this.traits & 0b0100) === 0b0100;
283+
} else if (typeof this.traits === "object") {
284+
return !!this.traits.idempotencyToken;
285+
}
286+
return false;
287+
}
288+
276289
/**
277290
* @returns own traits merged with member traits, where member traits of the same trait key take priority.
278291
* This method is cached.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, expect, test as it } from "vitest";
2+
3+
import { generateIdempotencyToken } from "./generateIdempotencyToken";
4+
5+
describe("generateIdempotencyToken", () => {
6+
// This test is not meaningful when using uuid v4 as an external package, but
7+
// will become useful if replacing the uuid implementation in the future.
8+
const tokens = {} as Record<string, boolean>;
9+
10+
it("should repeatedly generate uuid v4 strings", () => {
11+
for (let i = 0; i < 1000; ++i) {
12+
const token = generateIdempotencyToken();
13+
tokens[token] = true;
14+
expect(generateIdempotencyToken()).toMatch(
15+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
16+
);
17+
}
18+
19+
expect(Object.keys(tokens)).toHaveLength(1000);
20+
});
21+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { v4 as generateIdempotencyToken } from "uuid";
2+
3+
export { generateIdempotencyToken };

packages/core/src/submodules/serde/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./copyDocumentWithTransform";
22
export * from "./date-utils";
3+
export * from "./generateIdempotencyToken";
34
export * from "./lazy-json";
45
export * from "./parse-utils";
56
export * from "./quote-header";

0 commit comments

Comments
 (0)