Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions packages/core/src/submodules/protocols/idempotencyToken.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { CborShapeSerializer } from "@smithy/core/cbor";
import { sim, struct } from "@smithy/core/schema";
import { describe, expect, test as it } from "vitest";

import { JsonShapeSerializer } from "./json/JsonShapeSerializer";
import { QueryShapeSerializer } from "./query/QueryShapeSerializer";
import { XmlShapeSerializer } from "./xml/XmlShapeSerializer";

describe("idempotencyToken", () => {
const structureSchema = struct(
"ns",
"StructureWithIdempotencyToken",
0,
["idempotencyToken", "plain"],
[sim("ns", "IdempotencyTokenString", 0, 0b0100), sim("ns", "PlainString", 0, 0b0000)]
);

it("all ShapeSerializer implementations should generate an idempotency token if no input was provided by the caller", () => {
const serializers = [
new JsonShapeSerializer({
timestampFormat: { default: 7, useTrait: true },
jsonName: true,
}),
new QueryShapeSerializer({
timestampFormat: { default: 7, useTrait: true },
}),
new XmlShapeSerializer({
serviceNamespace: "ServiceNamespace",
timestampFormat: { default: 7, useTrait: true },
xmlNamespace: "XmlNamespace",
}),
new CborShapeSerializer(),
];

const expectedSerializations = [
/{"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"}/,
/&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/,
/<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>/,
/�pidempotencyTokenx\$([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})eplainhpotatoes/,
];

for (let i = 0; i < expectedSerializations.length; i++) {
const serializer = serializers[i];
const expectedSerialization = expectedSerializations[i];

// automatic token
{
serializer.write(structureSchema, {
idempotencyToken: undefined,
plain: "potatoes",
});
const data = serializer.flush();
const serialization = Buffer.from(data).toString("utf8");
expect(serialization).toMatch(expectedSerialization);
}

// manual token
{
serializer.write(structureSchema, {
idempotencyToken: "00000000-0000-4000-9000-000000000000",
plain: "potatoes",
});
const data = serializer.flush();
const serialization = Buffer.from(data).toString("utf8");
expect(serialization).toMatch(expectedSerialization);
}
}
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { NormalizedSchema, SCHEMA } from "@smithy/core/schema";
import { dateToUtcString } from "@smithy/core/serde";
import { LazyJsonString } from "@smithy/core/serde";
import { dateToUtcString, generateIdempotencyToken, LazyJsonString } from "@smithy/core/serde";
import { Schema, ShapeSerializer } from "@smithy/types";

import { SerdeContextConfig } from "../ConfigurableSerdeContext";
Expand Down Expand Up @@ -110,11 +109,18 @@ export class JsonShapeSerializer extends SerdeContextConfig implements ShapeSeri
}
}

const mediaType = ns.getMergedTraits().mediaType;
if (ns.isStringSchema() && typeof value === "string" && mediaType) {
const isJson = mediaType === "application/json" || mediaType.endsWith("+json");
if (isJson) {
return LazyJsonString.from(value);
if (ns.isStringSchema()) {
if (typeof value === "undefined" && ns.isIdempotencyToken()) {
return generateIdempotencyToken();
}

const mediaType = ns.getMergedTraits().mediaType;

if (typeof value === "string" && mediaType) {
const isJson = mediaType === "application/json" || mediaType.endsWith("+json");
if (isJson) {
return LazyJsonString.from(value);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class JsonReplacer {

return (key: string, value: unknown) => {
if (value instanceof NumericValue) {
const v = `${NUMERIC_CONTROL_CHAR + +"nv" + this.counter++}_` + value.string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

superfluous + symbol

const v = `${NUMERIC_CONTROL_CHAR + "nv" + this.counter++}_` + value.string;
this.values.set(`"${v}"`, value.string);
return v;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { determineTimestampFormat, extendedEncodeURIComponent } from "@smithy/core/protocols";
import { NormalizedSchema, SCHEMA } from "@smithy/core/schema";
import { NumericValue } from "@smithy/core/serde";
import { generateIdempotencyToken, NumericValue } from "@smithy/core/serde";
import { dateToUtcString } from "@smithy/smithy-client";
import type { Schema, ShapeSerializer } from "@smithy/types";
import { toBase64 } from "@smithy/util-base64";
Expand Down Expand Up @@ -36,6 +36,9 @@ export class QueryShapeSerializer extends SerdeContextConfig implements ShapeSer
if (value != null) {
this.writeKey(prefix);
this.writeValue(String(value));
} else if (ns.isIdempotencyToken()) {
this.writeKey(prefix);
this.writeValue(generateIdempotencyToken());
}
} else if (ns.isBigIntegerSchema()) {
if (value != null) {
Expand Down Expand Up @@ -111,7 +114,7 @@ export class QueryShapeSerializer extends SerdeContextConfig implements ShapeSer
} else if (ns.isStructSchema()) {
if (value && typeof value === "object") {
for (const [memberName, member] of ns.structIterator()) {
if ((value as any)[memberName] == null) {
if ((value as any)[memberName] == null && !member.isIdempotencyToken()) {
continue;
}
const suffix = this.getKey(memberName, member.getMergedTraits().xmlName);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { XmlNode, XmlText } from "@aws-sdk/xml-builder";
import { NormalizedSchema, SCHEMA } from "@smithy/core/schema";
import { NumericValue } from "@smithy/core/serde";
import { generateIdempotencyToken, NumericValue } from "@smithy/core/serde";
import { dateToUtcString } from "@smithy/smithy-client";
import type { Schema as ISchema, ShapeSerializer } from "@smithy/types";
import { fromBase64, toBase64 } from "@smithy/util-base64";
Expand Down Expand Up @@ -86,7 +86,7 @@ export class XmlShapeSerializer extends SerdeContextConfig implements ShapeSeria
for (const [memberName, memberSchema] of ns.structIterator()) {
const val = (value as any)[memberName];

if (val != null) {
if (val != null || memberSchema.isIdempotencyToken()) {
if (memberSchema.getMergedTraits().xmlAttribute) {
structXmlNode.addAttribute(
memberSchema.getMergedTraits().xmlName ?? memberName,
Expand Down Expand Up @@ -298,16 +298,18 @@ export class XmlShapeSerializer extends SerdeContextConfig implements ShapeSeria
}
}

if (
ns.isStringSchema() ||
ns.isBooleanSchema() ||
ns.isNumericSchema() ||
ns.isBigIntegerSchema() ||
ns.isBigDecimalSchema()
) {
if (ns.isBooleanSchema() || ns.isNumericSchema() || ns.isBigIntegerSchema() || ns.isBigDecimalSchema()) {
nodeContents = String(value);
}

if (ns.isStringSchema()) {
if (value === undefined && ns.isIdempotencyToken()) {
nodeContents = generateIdempotencyToken();
} else {
nodeContents = String(value);
}
}

if (nodeContents === null) {
throw new Error(`Unhandled schema-value pair ${ns.getName(true)}=${value}`);
}
Expand Down
Loading