diff --git a/.changeset/tame-suns-carry.md b/.changeset/tame-suns-carry.md new file mode 100644 index 00000000000..26b79823254 --- /dev/null +++ b/.changeset/tame-suns-carry.md @@ -0,0 +1,7 @@ +--- +"@smithy/util-base64": minor +"@smithy/util-stream": minor +"@smithy/core": minor +--- + +refactoring to reduce code size diff --git a/packages/core/src/submodules/cbor/CborCodec.ts b/packages/core/src/submodules/cbor/CborCodec.ts index a3d1f911cb2..1331cf79738 100644 --- a/packages/core/src/submodules/cbor/CborCodec.ts +++ b/packages/core/src/submodules/cbor/CborCodec.ts @@ -1,5 +1,5 @@ import { NormalizedSchema } from "@smithy/core/schema"; -import { generateIdempotencyToken, parseEpochTimestamp } from "@smithy/core/serde"; +import { _parseEpochTimestamp, generateIdempotencyToken } from "@smithy/core/serde"; import type { Codec, Schema, SerdeFunctions, ShapeDeserializer, ShapeSerializer } from "@smithy/types"; import { fromBase64 } from "@smithy/util-base64"; @@ -148,7 +148,7 @@ export class CborShapeDeserializer implements ShapeDeserializer { if (ns.isTimestampSchema() && typeof value === "number") { // format is ignored. - return parseEpochTimestamp(value); + return _parseEpochTimestamp(value); } if (ns.isBlobSchema()) { diff --git a/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.spec.ts b/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.spec.ts index 96627f9e05d..65bd1c6a0cd 100644 --- a/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.spec.ts +++ b/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.spec.ts @@ -1,7 +1,16 @@ -import type { ErrorSchema } from "@smithy/core/schema"; import { error, list, map, op, SCHEMA, struct, TypeRegistry } from "@smithy/core/schema"; import { HttpRequest, HttpResponse } from "@smithy/protocol-http"; -import type { ResponseMetadata, RetryableTrait, SchemaRef } from "@smithy/types"; +import type { + BlobSchema, + BooleanSchema, + MapSchemaModifier, + NumericSchema, + ResponseMetadata, + RetryableTrait, + SchemaRef, + StringSchema, + TimestampDefaultSchema, +} from "@smithy/types"; import { beforeEach, describe, expect, test as it } from "vitest"; import { cbor } from "./cbor"; @@ -29,8 +38,8 @@ describe(SmithyRpcV2CborProtocol.name, () => { {}, ["timestamp", "blob"], [ - [SCHEMA.TIMESTAMP_DEFAULT, 0], - [SCHEMA.BLOB, 0], + [4 satisfies TimestampDefaultSchema, 0], + [21 satisfies BlobSchema, 0], ] ), input: { @@ -56,11 +65,11 @@ describe(SmithyRpcV2CborProtocol.name, () => { {}, ["bool", "timestamp", "blob", "prefixHeaders", "searchParams"], [ - [SCHEMA.BOOLEAN, { httpQuery: "bool" }], - [SCHEMA.TIMESTAMP_DEFAULT, { httpHeader: "timestamp" }], - [SCHEMA.BLOB, { httpHeader: "blob" }], - [SCHEMA.MAP_MODIFIER | SCHEMA.STRING, { httpPrefixHeaders: "anti-" }], - [SCHEMA.MAP_MODIFIER | SCHEMA.STRING, { httpQueryParams: 1 }], + [2 satisfies BooleanSchema, { httpQuery: "bool" }], + [4 satisfies TimestampDefaultSchema, { httpHeader: "timestamp" }], + [21 satisfies BlobSchema, { httpHeader: "blob" }], + [(128 satisfies MapSchemaModifier) | (0 satisfies StringSchema), { httpPrefixHeaders: "anti-" }], + [(128 satisfies MapSchemaModifier) | (0 satisfies StringSchema), { httpQueryParams: 1 }], ] ), input: { @@ -104,10 +113,10 @@ describe(SmithyRpcV2CborProtocol.name, () => { 0, ["mySparseList", "myRegularList", "mySparseMap", "myRegularMap"], [ - [() => list("", "MySparseList", { sparse: 1 }, SCHEMA.NUMERIC), {}], - [() => list("", "MyList", {}, SCHEMA.NUMERIC), {}], - [() => map("", "MySparseMap", { sparse: 1 }, SCHEMA.STRING, SCHEMA.NUMERIC), {}], - [() => map("", "MyMap", {}, SCHEMA.STRING, SCHEMA.NUMERIC), {}], + [() => list("", "MySparseList", { sparse: 1 }, 1 satisfies NumericSchema), {}], + [() => list("", "MyList", {}, 1 satisfies NumericSchema), {}], + [() => map("", "MySparseMap", { sparse: 1 }, 0 satisfies StringSchema, 1 satisfies NumericSchema), {}], + [() => map("", "MyMap", {}, 0 satisfies StringSchema, 1 satisfies NumericSchema), {}], ] ), input: { @@ -207,10 +216,10 @@ describe(SmithyRpcV2CborProtocol.name, () => { 0, ["mySparseList", "myRegularList", "mySparseMap", "myRegularMap"], [ - [() => list("", "MyList", { sparse: 1 }, SCHEMA.NUMERIC), {}], - [() => list("", "MyList", {}, SCHEMA.NUMERIC), {}], - [() => map("", "MyMap", { sparse: 1 }, SCHEMA.STRING, SCHEMA.NUMERIC), {}], - [() => map("", "MyMap", {}, SCHEMA.STRING, SCHEMA.NUMERIC), {}], + [() => list("", "MyList", { sparse: 1 }, 1 satisfies NumericSchema), {}], + [() => list("", "MyList", {}, 1 satisfies NumericSchema), {}], + [() => map("", "MyMap", { sparse: 1 }, 0 satisfies StringSchema, 1 satisfies NumericSchema), {}], + [() => map("", "MyMap", {}, 0 satisfies StringSchema, 1 satisfies NumericSchema), {}], ] ), mockOutput: { diff --git a/packages/core/src/submodules/event-streams/EventStreamSerde.spec.ts b/packages/core/src/submodules/event-streams/EventStreamSerde.spec.ts index 0b8fef2098f..044554caea7 100644 --- a/packages/core/src/submodules/event-streams/EventStreamSerde.spec.ts +++ b/packages/core/src/submodules/event-streams/EventStreamSerde.spec.ts @@ -1,8 +1,14 @@ import { cbor, CborCodec, dateToTag } from "@smithy/core/cbor"; -import { NormalizedSchema, SCHEMA, sim, struct } from "@smithy/core/schema"; +import { NormalizedSchema, sim, struct } from "@smithy/core/schema"; import { EventStreamMarshaller } from "@smithy/eventstream-serde-node"; import { HttpResponse } from "@smithy/protocol-http"; -import type { Message as EventMessage } from "@smithy/types"; +import type { + BlobSchema, + Message as EventMessage, + StreamingBlobSchema, + StringSchema, + TimestampEpochSecondsSchema, +} from "@smithy/types"; import { fromUtf8, toUtf8 } from "@smithy/util-utf8"; import { describe, expect, test as it } from "vitest"; @@ -53,9 +59,15 @@ describe(EventStreamSerde.name, () => { "Payload", 0, ["payload"], - [sim("ns", "StreamingBlobPayload", SCHEMA.STREAMING_BLOB, { eventPayload: 1 })] + [sim("ns", "StreamingBlobPayload", 42 satisfies StreamingBlobSchema, { eventPayload: 1 })] + ), + struct( + "ns", + "TextPayload", + 0, + ["payload"], + [sim("ns", "TextPayload", 0 satisfies StringSchema, { eventPayload: 1 })] ), - struct("ns", "TextPayload", 0, ["payload"], [sim("ns", "TextPayload", SCHEMA.STRING, { eventPayload: 1 })]), struct( "ns", "CustomHeaders", @@ -73,7 +85,7 @@ describe(EventStreamSerde.name, () => { // here the non-eventstream members form an initial-request // or initial-response when present. ["eventStreamMember", "dateMember", "blobMember"], - [eventStreamUnionSchema, SCHEMA.TIMESTAMP_EPOCH_SECONDS, SCHEMA.BLOB] + [eventStreamUnionSchema, 7 satisfies TimestampEpochSecondsSchema, 21 satisfies BlobSchema] ); describe("serialization", () => { diff --git a/packages/core/src/submodules/event-streams/EventStreamSerde.ts b/packages/core/src/submodules/event-streams/EventStreamSerde.ts index 7bff737f50b..91531f9643d 100644 --- a/packages/core/src/submodules/event-streams/EventStreamSerde.ts +++ b/packages/core/src/submodules/event-streams/EventStreamSerde.ts @@ -1,6 +1,6 @@ -import type { NormalizedSchema } from "@smithy/core/schema"; -import { SCHEMA } from "@smithy/core/schema"; +import type { NormalizedSchema, StructureSchema } from "@smithy/core/schema"; import type { + DocumentSchema, EventStreamMarshaller, HttpRequest as IHttpRequest, HttpResponse as IHttpResponse, @@ -232,14 +232,17 @@ export class EventStreamSerde { let explicitPayloadMember = null as null | string; let explicitPayloadContentType: undefined | string; - const isKnownSchema = unionSchema.hasMemberSchema(unionMember); + const isKnownSchema = (() => { + const struct = unionSchema.getSchema() as StructureSchema; + return struct.memberNames.includes(unionMember); + })(); const additionalHeaders: MessageHeaders = {}; if (!isKnownSchema) { // $unknown member const [type, value] = event[unionMember]; eventType = type; - serializer.write(SCHEMA.DOCUMENT, value); + serializer.write(15 satisfies DocumentSchema, value); } else { const eventSchema = unionSchema.getMemberSchema(unionMember); diff --git a/packages/core/src/submodules/protocols/HttpBindingProtocol.spec.ts b/packages/core/src/submodules/protocols/HttpBindingProtocol.spec.ts index 98eba8f158f..ce6eed9b625 100644 --- a/packages/core/src/submodules/protocols/HttpBindingProtocol.spec.ts +++ b/packages/core/src/submodules/protocols/HttpBindingProtocol.spec.ts @@ -1,10 +1,12 @@ -import { map, op, SCHEMA, struct } from "@smithy/core/schema"; +import { map, op, struct } from "@smithy/core/schema"; import { HttpResponse } from "@smithy/protocol-http"; import type { Codec, CodecSettings, HandlerExecutionContext, HttpResponse as IHttpResponse, + ListSchemaModifier, + MapSchemaModifier, MetadataBearer, OperationSchema, ResponseMetadata, @@ -12,6 +14,9 @@ import type { SerdeFunctions, ShapeDeserializer, ShapeSerializer, + StringSchema, + TimestampDefaultSchema, + TimestampEpochSecondsSchema, } from "@smithy/types"; import { parseUrl } from "@smithy/url-parser/src"; import { describe, expect, test as it } from "vitest"; @@ -32,7 +37,7 @@ describe(HttpBindingProtocol.name, () => { const settings: CodecSettings = { timestampFormat: { useTrait: true, - default: SCHEMA.TIMESTAMP_EPOCH_SECONDS, + default: 7 satisfies TimestampEpochSecondsSchema, }, httpBindings: true, }; @@ -82,7 +87,7 @@ describe(HttpBindingProtocol.name, () => { ["timestampList"], [ [ - SCHEMA.LIST_MODIFIER | SCHEMA.TIMESTAMP_DEFAULT, + (64 satisfies ListSchemaModifier) | (4 satisfies TimestampDefaultSchema), { httpHeader: "x-timestamplist", }, @@ -122,7 +127,7 @@ describe(HttpBindingProtocol.name, () => { ["httpPrefixHeaders"], [ [ - SCHEMA.MAP_MODIFIER | SCHEMA.STRING, + (128 satisfies MapSchemaModifier) | (0 satisfies StringSchema), { httpPrefixHeaders: "", }, @@ -175,7 +180,7 @@ describe(HttpBindingProtocol.name, () => { httpBindings: true, timestampFormat: { useTrait: true, - default: SCHEMA.TIMESTAMP_EPOCH_SECONDS, + default: 7 satisfies TimestampEpochSecondsSchema, }, }), } diff --git a/packages/core/src/submodules/protocols/HttpBindingProtocol.ts b/packages/core/src/submodules/protocols/HttpBindingProtocol.ts index 5f38158f92f..b3266b13ea9 100644 --- a/packages/core/src/submodules/protocols/HttpBindingProtocol.ts +++ b/packages/core/src/submodules/protocols/HttpBindingProtocol.ts @@ -1,7 +1,8 @@ -import { NormalizedSchema, SCHEMA } from "@smithy/core/schema"; +import { NormalizedSchema, translateTraits } from "@smithy/core/schema"; import { splitEvery, splitHeader } from "@smithy/core/serde"; import { HttpRequest } from "@smithy/protocol-http"; import type { + DocumentSchema, Endpoint, EndpointBearer, HandlerExecutionContext, @@ -11,6 +12,7 @@ import type { OperationSchema, Schema, SerdeFunctions, + TimestampDefaultSchema, } from "@smithy/types"; import { sdkStreamMixin } from "@smithy/util-stream"; @@ -58,7 +60,7 @@ export abstract class HttpBindingProtocol extends HttpProtocol { if (endpoint) { this.updateServiceEndpoint(request, endpoint); this.setHostPrefix(request, operationSchema, input); - const opTraits = NormalizedSchema.translateTraits(operationSchema.traits); + const opTraits = translateTraits(operationSchema.traits); if (opTraits.http) { request.method = opTraits.http[0]; const [path, search] = opTraits.http[1].split("?"); @@ -203,7 +205,7 @@ export abstract class HttpBindingProtocol extends HttpProtocol { if (response.statusCode >= 300) { const bytes: Uint8Array = await collectBody(response.body, context); if (bytes.byteLength > 0) { - Object.assign(dataObject, await deserializer.read(SCHEMA.DOCUMENT, bytes)); + Object.assign(dataObject, await deserializer.read(15 satisfies DocumentSchema, bytes)); } await this.handleError(operationSchema, context, response, dataObject, this.deserializeMetadata(response)); throw new Error("@smithy/core/protocols - HTTP Protocol error handler failed to throw."); @@ -304,7 +306,7 @@ export abstract class HttpBindingProtocol extends HttpProtocol { let sections: string[]; if ( headerListValueSchema.isTimestampSchema() && - headerListValueSchema.getSchema() === SCHEMA.TIMESTAMP_DEFAULT + headerListValueSchema.getSchema() === (4 satisfies TimestampDefaultSchema) ) { sections = splitEvery(value, ",", 2); } else { diff --git a/packages/core/src/submodules/protocols/HttpProtocol.spec.ts b/packages/core/src/submodules/protocols/HttpProtocol.spec.ts index bf84daf1978..e825c8d67ef 100644 --- a/packages/core/src/submodules/protocols/HttpProtocol.spec.ts +++ b/packages/core/src/submodules/protocols/HttpProtocol.spec.ts @@ -1,5 +1,11 @@ -import { map, SCHEMA, struct } from "@smithy/core/schema"; -import type { HandlerExecutionContext, HttpResponse as IHttpResponse, Schema, SerdeFunctions } from "@smithy/types"; +import { map, struct } from "@smithy/core/schema"; +import type { + HandlerExecutionContext, + HttpResponse as IHttpResponse, + Schema, + SerdeFunctions, + TimestampEpochSecondsSchema, +} from "@smithy/types"; import { describe, expect, test as it } from "vitest"; import { HttpProtocol } from "./HttpProtocol"; @@ -18,7 +24,7 @@ describe(HttpProtocol.name, () => { httpBindings: true, timestampFormat: { useTrait: true, - default: SCHEMA.TIMESTAMP_EPOCH_SECONDS, + default: 7 satisfies TimestampEpochSecondsSchema, }, }), }); diff --git a/packages/core/src/submodules/protocols/HttpProtocol.ts b/packages/core/src/submodules/protocols/HttpProtocol.ts index ec984c79a68..f178e84ceee 100644 --- a/packages/core/src/submodules/protocols/HttpProtocol.ts +++ b/packages/core/src/submodules/protocols/HttpProtocol.ts @@ -73,10 +73,10 @@ export abstract class HttpProtocol implements ClientProtocol= 300) { const bytes: Uint8Array = await collectBody(response.body, context as SerdeFunctions); if (bytes.byteLength > 0) { - Object.assign(dataObject, await deserializer.read(SCHEMA.DOCUMENT, bytes)); + Object.assign(dataObject, await deserializer.read(15 satisfies DocumentSchema, bytes)); } await this.handleError(operationSchema, context, response, dataObject, this.deserializeMetadata(response)); throw new Error("@smithy/core/protocols - RPC Protocol error handler failed to throw."); diff --git a/packages/core/src/submodules/protocols/serde/FromStringShapeDeserializer.ts b/packages/core/src/submodules/protocols/serde/FromStringShapeDeserializer.ts index 8c1f1bd47cd..d3a1641d04b 100644 --- a/packages/core/src/submodules/protocols/serde/FromStringShapeDeserializer.ts +++ b/packages/core/src/submodules/protocols/serde/FromStringShapeDeserializer.ts @@ -1,13 +1,21 @@ -import { NormalizedSchema, SCHEMA } from "@smithy/core/schema"; +import { NormalizedSchema } from "@smithy/core/schema"; import { + _parseEpochTimestamp, + _parseRfc3339DateTimeWithOffset, + _parseRfc7231DateTime, LazyJsonString, NumericValue, - parseEpochTimestamp, - parseRfc3339DateTimeWithOffset, - parseRfc7231DateTime, splitHeader, } from "@smithy/core/serde"; -import type { CodecSettings, Schema, SerdeFunctions, ShapeDeserializer } from "@smithy/types"; +import type { + CodecSettings, + Schema, + SerdeFunctions, + ShapeDeserializer, + TimestampDateTimeSchema, + TimestampEpochSecondsSchema, + TimestampHttpDateSchema, +} from "@smithy/types"; import { fromBase64 } from "@smithy/util-base64"; import { toUtf8 } from "@smithy/util-utf8"; @@ -42,12 +50,12 @@ export class FromStringShapeDeserializer implements ShapeDeserializer { const format = determineTimestampFormat(ns, this.settings); switch (format) { - case SCHEMA.TIMESTAMP_DATE_TIME: - return parseRfc3339DateTimeWithOffset(data); - case SCHEMA.TIMESTAMP_HTTP_DATE: - return parseRfc7231DateTime(data); - case SCHEMA.TIMESTAMP_EPOCH_SECONDS: - return parseEpochTimestamp(data); + case 5 satisfies TimestampDateTimeSchema: + return _parseRfc3339DateTimeWithOffset(data); + case 6 satisfies TimestampHttpDateSchema: + return _parseRfc7231DateTime(data); + case 7 satisfies TimestampEpochSecondsSchema: + return _parseEpochTimestamp(data); default: console.warn("Missing timestamp format, parsing value with Date constructor:", data); return new Date(data as string | number); @@ -69,15 +77,17 @@ export class FromStringShapeDeserializer implements ShapeDeserializer { } } - switch (true) { - case ns.isNumericSchema(): - return Number(data); - case ns.isBigIntegerSchema(): - return BigInt(data); - case ns.isBigDecimalSchema(): - return new NumericValue(data, "bigDecimal"); - case ns.isBooleanSchema(): - return String(data).toLowerCase() === "true"; + if (ns.isNumericSchema()) { + return Number(data); + } + if (ns.isBigIntegerSchema()) { + return BigInt(data); + } + if (ns.isBigDecimalSchema()) { + return new NumericValue(data, "bigDecimal"); + } + if (ns.isBooleanSchema()) { + return String(data).toLowerCase() === "true"; } return data; } diff --git a/packages/core/src/submodules/protocols/serde/ToStringShapeSerializer.ts b/packages/core/src/submodules/protocols/serde/ToStringShapeSerializer.ts index a16960b4c30..e318f8133c2 100644 --- a/packages/core/src/submodules/protocols/serde/ToStringShapeSerializer.ts +++ b/packages/core/src/submodules/protocols/serde/ToStringShapeSerializer.ts @@ -1,6 +1,14 @@ -import { NormalizedSchema, SCHEMA } from "@smithy/core/schema"; +import { NormalizedSchema } from "@smithy/core/schema"; import { dateToUtcString, LazyJsonString, quoteHeader } from "@smithy/core/serde"; -import type { CodecSettings, Schema, SerdeFunctions, ShapeSerializer } from "@smithy/types"; +import type { + CodecSettings, + Schema, + SerdeFunctions, + ShapeSerializer, + TimestampDateTimeSchema, + TimestampEpochSecondsSchema, + TimestampHttpDateSchema, +} from "@smithy/types"; import { toBase64 } from "@smithy/util-base64"; import { determineTimestampFormat } from "./determineTimestampFormat"; @@ -38,13 +46,13 @@ export class ToStringShapeSerializer implements ShapeSerializer { } const format = determineTimestampFormat(ns, this.settings); switch (format) { - case SCHEMA.TIMESTAMP_DATE_TIME: + case 5 satisfies TimestampDateTimeSchema: this.stringBuffer = value.toISOString().replace(".000Z", "Z"); break; - case SCHEMA.TIMESTAMP_HTTP_DATE: + case 6 satisfies TimestampHttpDateSchema: this.stringBuffer = dateToUtcString(value); break; - case SCHEMA.TIMESTAMP_EPOCH_SECONDS: + case 7 satisfies TimestampEpochSecondsSchema: this.stringBuffer = String(value.getTime() / 1000); break; default: diff --git a/packages/core/src/submodules/protocols/serde/determineTimestampFormat.ts b/packages/core/src/submodules/protocols/serde/determineTimestampFormat.ts index 1e9d48b39ae..9c31218aad6 100644 --- a/packages/core/src/submodules/protocols/serde/determineTimestampFormat.ts +++ b/packages/core/src/submodules/protocols/serde/determineTimestampFormat.ts @@ -1,5 +1,4 @@ import type { NormalizedSchema } from "@smithy/core/schema"; -import { SCHEMA } from "@smithy/core/schema"; import type { CodecSettings, TimestampDateTimeSchema, @@ -20,9 +19,9 @@ export function determineTimestampFormat( if (settings.timestampFormat.useTrait) { if ( ns.isTimestampSchema() && - (ns.getSchema() === SCHEMA.TIMESTAMP_DATE_TIME || - ns.getSchema() === SCHEMA.TIMESTAMP_HTTP_DATE || - ns.getSchema() === SCHEMA.TIMESTAMP_EPOCH_SECONDS) + (ns.getSchema() === (5 satisfies TimestampDateTimeSchema) || + ns.getSchema() === (6 satisfies TimestampHttpDateSchema) || + ns.getSchema() === (7 satisfies TimestampEpochSecondsSchema)) ) { return ns.getSchema() as TimestampDateTimeSchema | TimestampHttpDateSchema | TimestampEpochSecondsSchema; } @@ -32,9 +31,9 @@ export function determineTimestampFormat( const bindingFormat = settings.httpBindings ? typeof httpPrefixHeaders === "string" || Boolean(httpHeader) - ? SCHEMA.TIMESTAMP_HTTP_DATE + ? (6 satisfies TimestampHttpDateSchema) : Boolean(httpQuery) || Boolean(httpLabel) - ? SCHEMA.TIMESTAMP_DATE_TIME + ? (5 satisfies TimestampDateTimeSchema) : undefined : undefined; diff --git a/packages/core/src/submodules/schema/index.ts b/packages/core/src/submodules/schema/index.ts index 994b18403a2..5229be011ea 100644 --- a/packages/core/src/submodules/schema/index.ts +++ b/packages/core/src/submodules/schema/index.ts @@ -9,4 +9,5 @@ export * from "./schemas/Schema"; export * from "./schemas/SimpleSchema"; export * from "./schemas/StructureSchema"; export * from "./schemas/sentinels"; +export * from "./schemas/translateTraits"; export * from "./TypeRegistry"; diff --git a/packages/core/src/submodules/schema/schemas/NormalizedSchema.spec.ts b/packages/core/src/submodules/schema/schemas/NormalizedSchema.spec.ts index 36a868faaac..ef8251b70d1 100644 --- a/packages/core/src/submodules/schema/schemas/NormalizedSchema.spec.ts +++ b/packages/core/src/submodules/schema/schemas/NormalizedSchema.spec.ts @@ -1,12 +1,26 @@ -import type { MemberSchema, StructureSchema } from "@smithy/types"; +import type { + BigDecimalSchema, + BigIntegerSchema, + BlobSchema, + BooleanSchema, + DocumentSchema, + ListSchemaModifier, + MapSchemaModifier, + MemberSchema, + NumericSchema, + StreamingBlobSchema, + StringSchema, + StructureSchema, + TimestampDefaultSchema, +} from "@smithy/types"; import { describe, expect, test as it } from "vitest"; import { list } from "./ListSchema"; import { map } from "./MapSchema"; import { NormalizedSchema } from "./NormalizedSchema"; -import { SCHEMA } from "./sentinels"; import { sim } from "./SimpleSchema"; import { struct } from "./StructureSchema"; +import { translateTraits } from "./translateTraits"; describe(NormalizedSchema.name, () => { const [List, Map, Struct] = [list("ack", "List", { sparse: 1 }, 0), map("ack", "Map", 0, 0, 1), () => schema]; @@ -39,31 +53,31 @@ describe(NormalizedSchema.name, () => { }); it("translates a bitvector of traits to a traits object", () => { - expect(NormalizedSchema.translateTraits(0b0000_0000)).toEqual({}); - expect(NormalizedSchema.translateTraits(0b0000_0001)).toEqual({ + expect(translateTraits(0b0000_0000)).toEqual({}); + expect(translateTraits(0b0000_0001)).toEqual({ httpLabel: 1, }); - expect(NormalizedSchema.translateTraits(0b0000_0011)).toEqual({ + expect(translateTraits(0b0000_0011)).toEqual({ httpLabel: 1, idempotent: 1, }); - expect(NormalizedSchema.translateTraits(0b0000_0110)).toEqual({ + expect(translateTraits(0b0000_0110)).toEqual({ idempotent: 1, idempotencyToken: 1, }); - expect(NormalizedSchema.translateTraits(0b0000_1100)).toEqual({ + expect(translateTraits(0b0000_1100)).toEqual({ idempotencyToken: 1, sensitive: 1, }); - expect(NormalizedSchema.translateTraits(0b0001_1000)).toEqual({ + expect(translateTraits(0b0001_1000)).toEqual({ sensitive: 1, httpPayload: 1, }); - expect(NormalizedSchema.translateTraits(0b0011_0000)).toEqual({ + expect(translateTraits(0b0011_0000)).toEqual({ httpPayload: 1, httpResponseCode: 1, }); - expect(NormalizedSchema.translateTraits(0b0110_0000)).toEqual({ + expect(translateTraits(0b0110_0000)).toEqual({ httpResponseCode: 1, httpQueryParams: 1, }); @@ -79,39 +93,37 @@ describe(NormalizedSchema.name, () => { expect(member.getSchema()).toBe(List); expect(member.getMemberName()).toBe("list"); }); - - it("throws when treating a non-member schema as a member schema", () => { - expect(() => { - ns.getMemberName(); - }).toThrow(); - }); }); describe("traversal and type identifiers", () => { it("type identifiers", () => { expect(NormalizedSchema.of("unit").isUnitSchema()).toBe(true); - expect(NormalizedSchema.of(SCHEMA.LIST_MODIFIER | SCHEMA.NUMERIC).isListSchema()).toBe(true); - expect(NormalizedSchema.of(SCHEMA.MAP_MODIFIER | SCHEMA.NUMERIC).isMapSchema()).toBe(true); + expect(NormalizedSchema.of((64 satisfies ListSchemaModifier) | (1 satisfies NumericSchema)).isListSchema()).toBe( + true + ); + expect(NormalizedSchema.of((128 satisfies MapSchemaModifier) | (1 satisfies NumericSchema)).isMapSchema()).toBe( + true + ); - expect(NormalizedSchema.of(SCHEMA.DOCUMENT).isDocumentSchema()).toBe(true); + expect(NormalizedSchema.of(15 satisfies DocumentSchema).isDocumentSchema()).toBe(true); expect(NormalizedSchema.of(ns.getMemberSchema("struct")).isStructSchema()).toBe(true); - expect(NormalizedSchema.of(SCHEMA.BLOB).isBlobSchema()).toBe(true); - expect(NormalizedSchema.of(SCHEMA.TIMESTAMP_DEFAULT).isTimestampSchema()).toBe(true); + expect(NormalizedSchema.of(21 satisfies BlobSchema).isBlobSchema()).toBe(true); + expect(NormalizedSchema.of(4 satisfies TimestampDefaultSchema).isTimestampSchema()).toBe(true); - expect(NormalizedSchema.of(SCHEMA.STRING).isStringSchema()).toBe(true); - expect(NormalizedSchema.of(SCHEMA.BOOLEAN).isBooleanSchema()).toBe(true); - expect(NormalizedSchema.of(SCHEMA.NUMERIC).isNumericSchema()).toBe(true); - expect(NormalizedSchema.of(SCHEMA.BIG_INTEGER).isBigIntegerSchema()).toBe(true); - expect(NormalizedSchema.of(SCHEMA.BIG_DECIMAL).isBigDecimalSchema()).toBe(true); - expect(NormalizedSchema.of(SCHEMA.STREAMING_BLOB).isStreaming()).toBe(true); + expect(NormalizedSchema.of(0 satisfies StringSchema).isStringSchema()).toBe(true); + expect(NormalizedSchema.of(2 satisfies BooleanSchema).isBooleanSchema()).toBe(true); + expect(NormalizedSchema.of(1 satisfies NumericSchema).isNumericSchema()).toBe(true); + expect(NormalizedSchema.of(17 satisfies BigIntegerSchema).isBigIntegerSchema()).toBe(true); + expect(NormalizedSchema.of(19 satisfies BigDecimalSchema).isBigDecimalSchema()).toBe(true); + expect(NormalizedSchema.of(42 satisfies StreamingBlobSchema).isStreaming()).toBe(true); const structWithStreamingMember = struct( "ack", "StructWithStreamingMember", 0, ["m"], - [sim("ns", "blob", SCHEMA.BLOB, { streaming: 1 })] + [sim("ns", "blob", 21 as BlobSchema, { streaming: 1 })] ); expect(NormalizedSchema.of(structWithStreamingMember).getMemberSchema("m").isStreaming()).toBe(true); }); @@ -149,6 +161,11 @@ describe(NormalizedSchema.name, () => { expect(member.getSchema()).toBe(0); expect(member.getMemberName()).toBe("key"); }); + it("should return a defined key schema even if the map was defined by a numeric sentinel value", () => { + const map = NormalizedSchema.of((128 satisfies MapSchemaModifier) | (1 satisfies NumericSchema)); + expect(map.getKeySchema().isStringSchema()).toBe(true); + expect(map.getValueSchema().isNumericSchema()).toBe(true); + }); it("map value member", () => { const member = ns.getMemberSchema("map").getValueSchema(); expect(member.isMemberSchema()).toBe(true); @@ -181,15 +198,6 @@ describe(NormalizedSchema.name, () => { }); }); - it("can identify whether a member exists", () => { - expect(ns.hasMemberSchema("list")).toBe(true); - expect(ns.hasMemberSchema("map")).toBe(true); - expect(ns.hasMemberSchema("struct")).toBe(true); - expect(ns.hasMemberSchema("a")).toBe(false); - expect(ns.hasMemberSchema("b")).toBe(false); - expect(ns.hasMemberSchema("c")).toBe(false); - }); - describe("iteration", () => { it("iterates over member schemas", () => { const iteration = Array.from(ns.structIterator()) as [string, NormalizedSchema][]; diff --git a/packages/core/src/submodules/schema/schemas/NormalizedSchema.ts b/packages/core/src/submodules/schema/schemas/NormalizedSchema.ts index 55887721d47..83b187b931d 100644 --- a/packages/core/src/submodules/schema/schemas/NormalizedSchema.ts +++ b/packages/core/src/submodules/schema/schemas/NormalizedSchema.ts @@ -1,19 +1,33 @@ import type { + BigDecimalSchema, + BigIntegerSchema, + BlobSchema, + BooleanSchema, + DocumentSchema, + ListSchemaModifier, + MapSchemaModifier, MemberSchema, NormalizedSchema as INormalizedSchema, + NumericSchema, Schema as ISchema, SchemaRef, SchemaTraits, SchemaTraitsObject, + StreamingBlobSchema, + StringSchema, + TimestampDefaultSchema, + TimestampEpochSecondsSchema, + UnitSchema, } from "@smithy/types"; +import type { IdempotencyTokenBitMask, TraitBitVector } from "@smithy/types/src/schema/traits"; import { deref } from "../deref"; import { ListSchema } from "./ListSchema"; import { MapSchema } from "./MapSchema"; import { Schema } from "./Schema"; -import { SCHEMA } from "./sentinels"; -import { SimpleSchema } from "./SimpleSchema"; +import type { SimpleSchema } from "./SimpleSchema"; import { StructureSchema } from "./StructureSchema"; +import { translateTraits } from "./translateTraits"; /** * Wraps both class instances, numeric sentinel values, and member schema pairs. @@ -22,6 +36,13 @@ import { StructureSchema } from "./StructureSchema"; * @alpha */ export class NormalizedSchema implements INormalizedSchema { + // ======================== + // + // This class implementation may be a little bit code-golfed to save space. + // This class is core to all clients in schema-serde mode. + // For readability, add comments rather than code. + // + // ======================== public static readonly symbol = Symbol.for("@smithy/nor"); protected readonly symbol = NormalizedSchema.symbol; @@ -39,7 +60,7 @@ export class NormalizedSchema implements INormalizedSchema { */ private constructor( readonly ref: SchemaRef, - private memberName?: string + private readonly memberName?: string ) { const traitStack = [] as SchemaTraits[]; let _ref = ref; @@ -57,7 +78,7 @@ export class NormalizedSchema implements INormalizedSchema { this.memberTraits = {}; for (let i = traitStack.length - 1; i >= 0; --i) { const traitSet = traitStack[i]; - Object.assign(this.memberTraits, NormalizedSchema.translateTraits(traitSet)); + Object.assign(this.memberTraits, translateTraits(traitSet)); } } else { this.memberTraits = 0; @@ -80,8 +101,7 @@ export class NormalizedSchema implements INormalizedSchema { this.traits = 0; } - this.name = - (this.schema instanceof Schema ? this.schema.getName?.() : void 0) ?? this.memberName ?? this.getSchemaName(); + this.name = (this.schema instanceof Schema ? this.schema.getName?.() : void 0) ?? this.memberName ?? String(schema); if (this._isMemberSchema && !memberName) { throw new Error(`@smithy/core/schema - NormalizedSchema member init ${this.getName(true)} missing member name.`); @@ -96,61 +116,31 @@ export class NormalizedSchema implements INormalizedSchema { * Static constructor that attempts to avoid wrapping a NormalizedSchema within another. */ public static of(ref: SchemaRef): NormalizedSchema { - if (ref instanceof NormalizedSchema) { - return ref; + const sc = deref(ref); + if (sc instanceof NormalizedSchema) { + return sc; } - if (Array.isArray(ref)) { - const [ns, traits] = ref; + if (Array.isArray(sc)) { + const [ns, traits] = sc; if (ns instanceof NormalizedSchema) { - Object.assign(ns.getMergedTraits(), NormalizedSchema.translateTraits(traits)); + Object.assign(ns.getMergedTraits(), translateTraits(traits)); return ns; } // An aggregate schema must be initialized with members and the member retrieved through the aggregate // container. throw new Error(`@smithy/core/schema - may not init unwrapped member schema=${JSON.stringify(ref, null, 2)}.`); } - return new NormalizedSchema(ref); - } - - /** - * @param indicator - numeric indicator for preset trait combination. - * @returns equivalent trait object. - */ - public static translateTraits(indicator: SchemaTraits): SchemaTraitsObject { - if (typeof indicator === "object") { - return indicator; - } - indicator = indicator | 0; - const traits = {} as SchemaTraitsObject; - let i = 0; - for (const trait of [ - "httpLabel", - "idempotent", - "idempotencyToken", - "sensitive", - "httpPayload", - "httpResponseCode", - "httpQueryParams", - ] as Array) { - if (((indicator >> i++) & 1) === 1) { - traits[trait] = 1; - } - } - return traits; + return new NormalizedSchema(sc); } /** * @returns the underlying non-normalized schema. */ public getSchema(): Exclude { - if (this.schema instanceof NormalizedSchema) { - Object.assign(this, { schema: this.schema.getSchema() }); - return this.schema; - } - if (this.schema instanceof SimpleSchema) { - return deref(this.schema.schemaRef) as Exclude; - } - return deref(this.schema) as Exclude; + return deref((this.schema as SimpleSchema)?.schemaRef ?? this.schema) as Exclude< + ISchema, + MemberSchema | INormalizedSchema + >; } /** @@ -158,23 +148,16 @@ export class NormalizedSchema implements INormalizedSchema { * @returns e.g. `MyShape` or `com.namespace#MyShape`. */ public getName(withNamespace = false): string | undefined { - if (!withNamespace) { - if (this.name && this.name.includes("#")) { - return this.name.split("#")[1]; - } - } + const { name } = this; + const short = !withNamespace && name && name.includes("#"); // empty name should return as undefined - return this.name || undefined; + return short ? name.split("#")[1] : name || undefined; } /** * @returns the member name if the schema is a member schema. - * @throws Error when the schema isn't a member schema. */ public getMemberName(): string { - if (!this.isMemberSchema()) { - throw new Error(`@smithy/core/schema - non-member schema: ${this.getName(true)}`); - } return this.memberName!; } @@ -182,73 +165,73 @@ export class NormalizedSchema implements INormalizedSchema { return this._isMemberSchema; } - public isUnitSchema(): boolean { - return this.getSchema() === ("unit" as const); - } - /** * boolean methods on this class help control flow in shape serialization and deserialization. */ public isListSchema(): boolean { - const inner = this.getSchema(); - if (typeof inner === "number") { - return inner >= SCHEMA.LIST_MODIFIER && inner < SCHEMA.MAP_MODIFIER; - } - return inner instanceof ListSchema; + const sc = this.getSchema(); + return typeof sc === "number" + ? sc >= (64 satisfies ListSchemaModifier) && sc < (128 satisfies MapSchemaModifier) + : sc instanceof ListSchema; } public isMapSchema(): boolean { - const inner = this.getSchema(); - if (typeof inner === "number") { - return inner >= SCHEMA.MAP_MODIFIER && inner <= 0b1111_1111; - } - return inner instanceof MapSchema; + const sc = this.getSchema(); + return typeof sc === "number" + ? sc >= (128 satisfies MapSchemaModifier) && sc <= 0b1111_1111 + : sc instanceof MapSchema; } public isStructSchema(): boolean { - const inner = this.getSchema(); - return (inner !== null && typeof inner === "object" && "members" in inner) || inner instanceof StructureSchema; + const sc = this.getSchema(); + return (sc !== null && typeof sc === "object" && "members" in sc) || sc instanceof StructureSchema; } public isBlobSchema(): boolean { - return this.getSchema() === SCHEMA.BLOB || this.getSchema() === SCHEMA.STREAMING_BLOB; + const sc = this.getSchema(); + return sc === (21 satisfies BlobSchema) || sc === (42 satisfies StreamingBlobSchema); } public isTimestampSchema(): boolean { - const schema = this.getSchema(); - return typeof schema === "number" && schema >= SCHEMA.TIMESTAMP_DEFAULT && schema <= SCHEMA.TIMESTAMP_EPOCH_SECONDS; + const sc = this.getSchema(); + return ( + typeof sc === "number" && + sc >= (4 satisfies TimestampDefaultSchema) && + sc <= (7 satisfies TimestampEpochSecondsSchema) + ); + } + + public isUnitSchema(): boolean { + return this.getSchema() === ("unit" satisfies UnitSchema); } public isDocumentSchema(): boolean { - return this.getSchema() === SCHEMA.DOCUMENT; + return this.getSchema() === (15 satisfies DocumentSchema); } public isStringSchema(): boolean { - return this.getSchema() === SCHEMA.STRING; + return this.getSchema() === (0 satisfies StringSchema); } public isBooleanSchema(): boolean { - return this.getSchema() === SCHEMA.BOOLEAN; + return this.getSchema() === (2 satisfies BooleanSchema); } public isNumericSchema(): boolean { - return this.getSchema() === SCHEMA.NUMERIC; + return this.getSchema() === (1 satisfies NumericSchema); } public isBigIntegerSchema(): boolean { - return this.getSchema() === SCHEMA.BIG_INTEGER; + return this.getSchema() === (17 satisfies BigIntegerSchema); } public isBigDecimalSchema(): boolean { - return this.getSchema() === SCHEMA.BIG_DECIMAL; + return this.getSchema() === (19 satisfies BigDecimalSchema); } public isStreaming(): boolean { - const streaming = !!this.getMergedTraits().streaming; - if (streaming) { - return true; - } - return this.getSchema() === SCHEMA.STREAMING_BLOB; + const { streaming } = this.getMergedTraits(); + return !!streaming || this.getSchema() === (42 satisfies StreamingBlobSchema); } /** @@ -256,21 +239,14 @@ export class NormalizedSchema implements INormalizedSchema { * @returns whether the schema has the idempotencyToken trait. */ public isIdempotencyToken(): boolean { - if (this.normalizedTraits) { - return !!this.normalizedTraits.idempotencyToken; - } - for (const traits of [this.traits, this.memberTraits]) { - if (typeof traits === "number") { - if ((traits & 0b0100) === 0b0100) { - return true; - } - } else if (typeof traits === "object") { - if (!!traits.idempotencyToken) { - return true; - } - } - } - return false; + // it is ok to perform the & operation on a trait object, + // since its int32 representation is 0. + const match = (traits?: SchemaTraits) => + ((traits as TraitBitVector) & (0b0100 satisfies IdempotencyTokenBitMask)) === 0b0100 || + !!(traits as SchemaTraitsObject)?.idempotencyToken; + + const { normalizedTraits, traits, memberTraits } = this; + return match(normalizedTraits) || match(traits) || match(memberTraits); } /** @@ -291,7 +267,7 @@ export class NormalizedSchema implements INormalizedSchema { * @returns only the member traits. If the schema is not a member, this returns empty. */ public getMemberTraits(): SchemaTraitsObject { - return NormalizedSchema.translateTraits(this.memberTraits); + return translateTraits(this.memberTraits); } /** @@ -299,7 +275,7 @@ export class NormalizedSchema implements INormalizedSchema { * If there are any member traits they are excluded. */ public getOwnTraits(): SchemaTraitsObject { - return NormalizedSchema.translateTraits(this.traits); + return translateTraits(this.traits); } /** @@ -308,17 +284,15 @@ export class NormalizedSchema implements INormalizedSchema { * @throws Error if the schema is not a Map or Document. */ public getKeySchema(): NormalizedSchema { - if (this.isDocumentSchema()) { - return this.memberFrom([SCHEMA.DOCUMENT, 0], "key"); - } - if (!this.isMapSchema()) { + const [isDoc, isMap] = [this.isDocumentSchema(), this.isMapSchema()]; + if (!isDoc && !isMap) { throw new Error(`@smithy/core/schema - cannot get key for non-map: ${this.getName(true)}`); } const schema = this.getSchema(); - if (typeof schema === "number") { - return this.memberFrom([0b0011_1111 & schema, 0], "key"); - } - return this.memberFrom([(schema as MapSchema).keySchema, 0], "key"); + const memberSchema = isDoc + ? (15 satisfies DocumentSchema) + : (schema as MapSchema)?.keySchema ?? (0 satisfies StringSchema); + return member([memberSchema, 0], "key"); } /** @@ -328,71 +302,41 @@ export class NormalizedSchema implements INormalizedSchema { * @throws Error if the schema is not a Map, List, nor Document. */ public getValueSchema(): NormalizedSchema { - const schema = this.getSchema(); - - if (typeof schema === "number") { - if (this.isMapSchema()) { - return this.memberFrom([0b0011_1111 & schema, 0], "value"); - } else if (this.isListSchema()) { - return this.memberFrom([0b0011_1111 & schema, 0], "member"); - } - } - - if (schema && typeof schema === "object") { - if (this.isStructSchema()) { - throw new Error(`may not getValueSchema() on structure ${this.getName(true)}`); - } - const collection = schema as MapSchema | ListSchema; - if ("valueSchema" in collection) { - if (this.isMapSchema()) { - return this.memberFrom([collection.valueSchema, 0], "value"); - } else if (this.isListSchema()) { - return this.memberFrom([collection.valueSchema, 0], "member"); - } - } - } - - if (this.isDocumentSchema()) { - return this.memberFrom([SCHEMA.DOCUMENT, 0], "value"); + const sc = this.getSchema(); + const [isDoc, isMap, isList] = [this.isDocumentSchema(), this.isMapSchema(), this.isListSchema()]; + const memberSchema = + typeof sc === "number" + ? 0b0011_1111 & sc + : sc && typeof sc === "object" && (isMap || isList) + ? ((sc as MapSchema | ListSchema).valueSchema as typeof sc) + : isDoc + ? (15 satisfies DocumentSchema) + : void 0; + if (memberSchema != null) { + return member([memberSchema, 0], isMap ? "value" : "member"); } - throw new Error(`@smithy/core/schema - ${this.getName(true)} has no value member.`); } - /** - * @param member - to query. - * @returns whether there is a memberSchema with the given member name. False if not a structure (or union). - */ - public hasMemberSchema(member: string): boolean { - if (this.isStructSchema()) { - const struct = this.getSchema() as StructureSchema; - return struct.memberNames.includes(member); - } - return false; - } - /** * @returns the NormalizedSchema for the given member name. The returned instance will return true for `isMemberSchema()` * and will have the member name given. - * @param member - which member to retrieve and wrap. + * @param memberName - which member to retrieve and wrap. * * @throws Error if member does not exist or the schema is neither a document nor structure. * Note that errors are assumed to be structures and unions are considered structures for these purposes. */ - public getMemberSchema(member: string): NormalizedSchema { - if (this.isStructSchema()) { - const struct = this.getSchema() as StructureSchema; - if (!struct.memberNames.includes(member)) { - throw new Error(`@smithy/core/schema - ${this.getName(true)} has no member=${member}.`); - } - const i = struct.memberNames.indexOf(member); + public getMemberSchema(memberName: string): NormalizedSchema { + const struct = this.getSchema() as StructureSchema; + if (this.isStructSchema() && struct.memberNames.includes(memberName)) { + const i = struct.memberNames.indexOf(memberName); const memberSchema = struct.memberList[i]; - return this.memberFrom(Array.isArray(memberSchema) ? memberSchema : [memberSchema, 0], member); + return member(Array.isArray(memberSchema) ? memberSchema : [memberSchema, 0], memberName); } if (this.isDocumentSchema()) { - return this.memberFrom([SCHEMA.DOCUMENT, 0], member); + return member([15 satisfies DocumentSchema, 0], memberName); } - throw new Error(`@smithy/core/schema - ${this.getName(true)} has no members.`); + throw new Error(`@smithy/core/schema - ${this.getName(true)} has no no member=${memberName}.`); } /** @@ -445,44 +389,23 @@ export class NormalizedSchema implements INormalizedSchema { } const struct = this.getSchema() as StructureSchema; for (let i = 0; i < struct.memberNames.length; ++i) { - yield [struct.memberNames[i], this.memberFrom([struct.memberList[i], 0], struct.memberNames[i])]; + yield [struct.memberNames[i], member([struct.memberList[i], 0], struct.memberNames[i])]; } } +} - /** - * Creates a normalized member schema from the given schema and member name. - */ - private memberFrom(memberSchema: NormalizedSchema | [SchemaRef, SchemaTraits], memberName: string): NormalizedSchema { - if (memberSchema instanceof NormalizedSchema) { - return Object.assign(memberSchema, { - memberName, - _isMemberSchema: true, - }); - } - return new NormalizedSchema(memberSchema, memberName); - } - - /** - * @returns a last-resort human-readable name for the schema if it has no other identifiers. - */ - private getSchemaName(): string { - const schema = this.getSchema(); - if (typeof schema === "number") { - const _schema = 0b0011_1111 & schema; - const container = 0b1100_0000 & schema; - const type = - Object.entries(SCHEMA).find(([, value]) => { - return value === _schema; - })?.[0] ?? "Unknown"; - switch (container) { - case SCHEMA.MAP_MODIFIER: - return `${type}Map`; - case SCHEMA.LIST_MODIFIER: - return `${type}List`; - case 0: - return type; - } - } - return "Unknown"; - } +/** + * Creates a normalized member schema from the given schema and member name. + * + * @internal + */ +function member(memberSchema: NormalizedSchema | [SchemaRef, SchemaTraits], memberName: string): NormalizedSchema { + if (memberSchema instanceof NormalizedSchema) { + return Object.assign(memberSchema, { + memberName, + _isMemberSchema: true, + }); + } + const internalCtorAccess = NormalizedSchema as any; + return new internalCtorAccess(memberSchema, memberName); } diff --git a/packages/core/src/submodules/schema/schemas/schemas.spec.ts b/packages/core/src/submodules/schema/schemas/schemas.spec.ts index b6e13c448e3..81422f9f34f 100644 --- a/packages/core/src/submodules/schema/schemas/schemas.spec.ts +++ b/packages/core/src/submodules/schema/schemas/schemas.spec.ts @@ -7,32 +7,10 @@ import { list, ListSchema } from "./ListSchema"; import { map, MapSchema } from "./MapSchema"; import { op, OperationSchema } from "./OperationSchema"; import { Schema } from "./Schema"; -import { SCHEMA } from "./sentinels"; import { sim, SimpleSchema } from "./SimpleSchema"; import { struct, StructureSchema } from "./StructureSchema"; describe("schemas", () => { - describe("sentinels", () => { - it("should be constant", () => { - expect(SCHEMA).toEqual({ - BLOB: 0b0001_0101, // 21 - STREAMING_BLOB: 0b0010_1010, // 42 - BOOLEAN: 0b0000_0010, // 2 - STRING: 0b0000_0000, // 0 - NUMERIC: 0b0000_0001, // 1 - BIG_INTEGER: 0b0001_0001, // 17 - BIG_DECIMAL: 0b0001_0011, // 19 - DOCUMENT: 0b0000_1111, // 15 - TIMESTAMP_DEFAULT: 0b0000_0100, // 4 - TIMESTAMP_DATE_TIME: 0b0000_0101, // 5 - TIMESTAMP_HTTP_DATE: 0b0000_0110, // 6 - TIMESTAMP_EPOCH_SECONDS: 0b0000_0111, // 7 - LIST_MODIFIER: 0b0100_0000, // 64 - MAP_MODIFIER: 0b1000_0000, // 128 - }); - }); - }); - describe(ErrorSchema.name, () => { const schema = error("ack", "Error", 0, [], []); diff --git a/packages/core/src/submodules/schema/schemas/sentinels.ts b/packages/core/src/submodules/schema/schemas/sentinels.ts index 2a2580df628..da7702ac24d 100644 --- a/packages/core/src/submodules/schema/schemas/sentinels.ts +++ b/packages/core/src/submodules/schema/schemas/sentinels.ts @@ -18,6 +18,8 @@ import type { /** * Schema sentinel runtime values. * @alpha + * + * @deprecated use inline numbers with type annotation to save space. */ export const SCHEMA: { BLOB: BlobSchema; diff --git a/packages/core/src/submodules/schema/schemas/translateTraits.ts b/packages/core/src/submodules/schema/schemas/translateTraits.ts new file mode 100644 index 00000000000..3e2d10159d9 --- /dev/null +++ b/packages/core/src/submodules/schema/schemas/translateTraits.ts @@ -0,0 +1,29 @@ +import type { SchemaTraits, SchemaTraitsObject } from "@smithy/types"; + +/** + * @internal + * @param indicator - numeric indicator for preset trait combination. + * @returns equivalent trait object. + */ +export function translateTraits(indicator: SchemaTraits): SchemaTraitsObject { + if (typeof indicator === "object") { + return indicator; + } + indicator = indicator | 0; + const traits = {} as SchemaTraitsObject; + let i = 0; + for (const trait of [ + "httpLabel", + "idempotent", + "idempotencyToken", + "sensitive", + "httpPayload", + "httpResponseCode", + "httpQueryParams", + ] as Array) { + if (((indicator >> i++) & 1) === 1) { + traits[trait] = 1; + } + } + return traits; +} diff --git a/packages/core/src/submodules/serde/index.ts b/packages/core/src/submodules/serde/index.ts index b6e01240a11..421ec8ea159 100644 --- a/packages/core/src/submodules/serde/index.ts +++ b/packages/core/src/submodules/serde/index.ts @@ -4,6 +4,7 @@ export * from "./generateIdempotencyToken"; export * from "./lazy-json"; export * from "./parse-utils"; export * from "./quote-header"; +export * from "./schema-serde-lib/schema-date-utils"; export * from "./split-every"; export * from "./split-header"; export * from "./value/NumericValue"; diff --git a/packages/core/src/submodules/serde/schema-serde-lib/schema-date-utils.spec.ts b/packages/core/src/submodules/serde/schema-serde-lib/schema-date-utils.spec.ts new file mode 100644 index 00000000000..902fe680d80 --- /dev/null +++ b/packages/core/src/submodules/serde/schema-serde-lib/schema-date-utils.spec.ts @@ -0,0 +1,421 @@ +import { describe, expect, it } from "vitest"; + +import { _parseEpochTimestamp, _parseRfc3339DateTimeWithOffset, _parseRfc7231DateTime } from "./schema-date-utils"; + +const millisecond = 1; +const second = 1000 * millisecond; +const minute = 60 * second; +const hour = 60 * minute; +const day = 24 * hour; +const year = 365 * day; + +describe("_parseEpochTimestamp", () => { + it("should parse numeric timestamps", () => { + expect(_parseEpochTimestamp(1234567890)).toEqual(new Date(1234567890000)); + expect(_parseEpochTimestamp(1234567890.123)).toEqual(new Date(1234567890123)); + expect(_parseEpochTimestamp(1234567890.123456)).toEqual(new Date(1234567890123)); + }); + + it("should parse string timestamps", () => { + expect(_parseEpochTimestamp("1234567890")).toEqual(new Date(1234567890000)); + expect(_parseEpochTimestamp("1234567890.123")).toEqual(new Date(1234567890123)); + expect(_parseEpochTimestamp("1234567890.123456")).toEqual(new Date(1234567890123)); + }); + + it("should parse CBOR tag timestamps", () => { + expect(_parseEpochTimestamp({ tag: 1, value: 1234567890 })).toEqual(new Date(1234567890000)); + expect(_parseEpochTimestamp({ tag: 1, value: 1234567890.123 })).toEqual(new Date(1234567890123)); + expect(_parseEpochTimestamp({ tag: 1, value: 1234567890.123456 })).toEqual(new Date(1234567890123)); + }); + + it("should return undefined for null/undefined", () => { + expect(_parseEpochTimestamp(null)).toBeUndefined(); + expect(_parseEpochTimestamp(undefined)).toBeUndefined(); + }); + + it("should throw for invalid numbers", () => { + expect(() => _parseEpochTimestamp("abc")).toThrow(); + expect(() => _parseEpochTimestamp(Infinity)).toThrow(); + expect(() => _parseEpochTimestamp(NaN)).toThrow(); + }); +}); + +describe("_parseRfc3339DateTimeWithOffset", () => { + it("should parse UTC timestamps", () => { + expect(_parseRfc3339DateTimeWithOffset("2077-12-25T00:00:00Z")).toEqual(new Date(3407616000_000)); + expect(_parseRfc3339DateTimeWithOffset("2077-12-25T12:12:12.01Z")).toEqual( + new Date(3407616000_000 + 12 * hour + 12 * minute + 12 * second + 10 * millisecond) + ); + expect(_parseRfc3339DateTimeWithOffset("2077-12-25T23:59:59.999Z")).toEqual( + new Date(3407616000_000 + 23 * hour + 59 * minute + 59 * second + 999 * millisecond) + ); + }); + + it("should parse timestamps with positive offset", () => { + expect(_parseRfc3339DateTimeWithOffset("2077-12-25T00:00:00-04:30")).toEqual(new Date(3407616000_000 + 4.5 * hour)); + expect(_parseRfc3339DateTimeWithOffset("2077-12-25T12:12:12.01-04:30")).toEqual( + new Date(3407616000_000 + 12 * hour + 12 * minute + 12 * second + 10 * millisecond + 4.5 * hour) + ); + expect(_parseRfc3339DateTimeWithOffset("2077-12-25T23:59:59.999-04:30")).toEqual( + new Date(3407616000_000 + 23 * hour + 59 * minute + 59 * second + 999 * millisecond + 4.5 * hour) + ); + }); + + it("should parse timestamps with negative offset", () => { + expect(_parseRfc3339DateTimeWithOffset("2077-12-25T00:00:00+05:00")).toEqual(new Date(3407616000_000 - 5 * hour)); + expect(_parseRfc3339DateTimeWithOffset("2077-12-25T12:12:12.01+05:00")).toEqual( + new Date(3407616000_000 + 12 * hour + 12 * minute + 12 * second + 10 * millisecond - 5 * hour) + ); + expect(_parseRfc3339DateTimeWithOffset("2077-12-25T23:59:59.999+05:00")).toEqual( + new Date(3407616000_000 + 23 * hour + 59 * minute + 59 * second + 999 * millisecond - 5 * hour) + ); + }); + + it("should parse timestamps with fractional seconds", () => { + expect(_parseRfc3339DateTimeWithOffset("2023-12-25T12:00:00.123Z")).toEqual(new Date("2023-12-25T12:00:00.123Z")); + }); + + it("should return undefined for null/undefined", () => { + expect(_parseRfc3339DateTimeWithOffset(null)).toBeUndefined(); + expect(_parseRfc3339DateTimeWithOffset(undefined)).toBeUndefined(); + }); + + it("should throw for invalid formats", () => { + expect(() => _parseRfc3339DateTimeWithOffset("2023-12-25")).toThrow(); + expect(() => _parseRfc3339DateTimeWithOffset(123)).toThrow(); + }); +}); + +describe("_parseRfc7231DateTime", () => { + it("should parse RFC7231 timestamps", () => { + expect(_parseRfc7231DateTime("Mon, 31 Dec 2077 23:59:30 GMT")).toEqual(new Date(3408220800000 - 30 * second)); + }); + + it("should parse timestamps with fractional seconds", () => { + expect(_parseRfc7231DateTime("Mon, 31 Dec 2077 23:59:30.123 GMT")).toEqual( + new Date(3408220800000 - 29 * second - 877 * millisecond) + ); + }); + + it("should parse RFC850 timestamps", () => { + expect(_parseRfc7231DateTime("Monday, 31-Dec-77 23:59:30 GMT")).toEqual( + new Date(3408220800000 - 100 * year - 25 * day - 30 * second) + ); + }); + + it("should parse asctime timestamps", () => { + expect(_parseRfc7231DateTime("Mon Dec 31 23:59:30 2077")).toEqual(new Date(3408220800000 - 30 * second)); + }); + + it("should return undefined for null/undefined", () => { + expect(_parseRfc7231DateTime(null)).toBeUndefined(); + expect(_parseRfc7231DateTime(undefined)).toBeUndefined(); + }); + + it("should throw for invalid formats", () => { + expect(() => _parseRfc7231DateTime("2077-12-25T08:49:37Z")).toThrow(); + expect(() => _parseRfc7231DateTime(123)).toThrow(); + expect(() => _parseRfc7231DateTime("Invalid, 25 Dec 2077 08:49:37 GMT")).toThrow(); + }); +}); + +// some invalid values are not validated client side +// because of excessive code requirements. +const invalidRfc3339DateTimes = [ + "85-04-12T23:20:50.52Z", // Year must be 4 digits + "985-04-12T23:20:50.52Z", // Year must be 4 digits + "1985-13-12T23:20:50.52Z", // Month cannot be greater than 12 + "1985-00-12T23:20:50.52Z", // Month cannot be 0 + "1985-4-12T23:20:50.52Z", // Month must be 2 digits with leading zero + "1985-04-32T23:20:50.52Z", // Day cannot be greater than 31 + "1985-04-00T23:20:50.52Z", // Day cannot be 0 + "1985-04-05T24:20:50.52Z", // Hours cannot be greater than 23 + "1985-04-05T23:61:50.52Z", // Minutes cannot be greater than 59 + "1985-04-05T23:20:61.52Z", // Seconds cannot be greater than 59 (except leap second) + // "1985-04-31T23:20:50.52Z", // April only has 30 days + // "2005-02-29T15:59:59Z", // 2005 was not a leap year, so February only had 28 days + "1996-12-19T16:39:57", // Missing timezone offset + "Mon, 31 Dec 1990 15:59:60 GMT", // RFC 7231 format, not RFC 3339 + "Monday, 31-Dec-90 15:59:60 GMT", // RFC 7231 format, not RFC 3339 + "Mon Dec 31 15:59:60 1990", // RFC 7231 format, not RFC 3339 + "1985-04-12T23:20:50.52Z1985-04-12T23:20:50.52Z", // Contains multiple timestamps + "1985-04-12T23:20:50.52ZA", // Contains invalid characters after timezone + "A1985-04-12T23:20:50.52Z", // Contains invalid characters before timestamp +]; + +describe("parseRfc3339DateTime", () => { + it.each([null, undefined])("returns undefined for %s", (value) => { + expect(_parseRfc3339DateTimeWithOffset(value)).toBeUndefined(); + }); + + describe("parses properly formatted dates", () => { + it("with fractional seconds", () => { + expect(_parseRfc3339DateTimeWithOffset("1985-04-12T23:20:50.52Z")).toEqual( + new Date(Date.UTC(1985, 3, 12, 23, 20, 50, 520)) + ); + }); + it("without fractional seconds", () => { + expect(_parseRfc3339DateTimeWithOffset("1985-04-12T23:20:50Z")).toEqual( + new Date(Date.UTC(1985, 3, 12, 23, 20, 50, 0)) + ); + }); + it("with leap seconds", () => { + expect(_parseRfc3339DateTimeWithOffset("1990-12-31T15:59:60Z")).toEqual( + new Date(Date.UTC(1990, 11, 31, 15, 59, 60, 0)) + ); + }); + it("with leap days", () => { + expect(_parseRfc3339DateTimeWithOffset("2004-02-29T15:59:59Z")).toEqual( + new Date(Date.UTC(2004, 1, 29, 15, 59, 59, 0)) + ); + }); + it("with leading zeroes", () => { + expect(_parseRfc3339DateTimeWithOffset("0004-02-09T05:09:09.09Z")).toEqual(new Date(-62037600650910)); + expect(_parseRfc3339DateTimeWithOffset("0004-02-09T00:00:00.00Z")).toEqual(new Date(-62037619200000)); + }); + }); + + it.each(invalidRfc3339DateTimes)("rejects %s", (value) => { + expect(() => _parseRfc3339DateTimeWithOffset(value)).toThrowError(); + }); +}); + +describe("parseRfc3339DateTimeWithOffset", () => { + it.each([null, undefined])("returns undefined for %s", (value) => { + expect(_parseRfc3339DateTimeWithOffset(value)).toBeUndefined(); + }); + + describe("parses properly formatted dates", () => { + it("with fractional seconds", () => { + expect(_parseRfc3339DateTimeWithOffset("1985-04-12T23:20:50.52Z")).toEqual( + new Date(Date.UTC(1985, 3, 12, 23, 20, 50, 520)) + ); + }); + it("without fractional seconds", () => { + expect(_parseRfc3339DateTimeWithOffset("1985-04-12T23:20:50Z")).toEqual( + new Date(Date.UTC(1985, 3, 12, 23, 20, 50, 0)) + ); + }); + it("with leap seconds", () => { + expect(_parseRfc3339DateTimeWithOffset("1990-12-31T15:59:60Z")).toEqual( + new Date(Date.UTC(1990, 11, 31, 15, 59, 60, 0)) + ); + }); + it("with leap days", () => { + expect(_parseRfc3339DateTimeWithOffset("2004-02-29T15:59:59Z")).toEqual( + new Date(Date.UTC(2004, 1, 29, 15, 59, 59, 0)) + ); + }); + it("with leading zeroes", () => { + expect(_parseRfc3339DateTimeWithOffset("0104-02-09T05:09:09.09Z")).toEqual( + new Date(Date.UTC(104, 1, 9, 5, 9, 9, 90)) + ); + expect(_parseRfc3339DateTimeWithOffset("0104-02-09T00:00:00.00Z")).toEqual( + new Date(Date.UTC(104, 1, 9, 0, 0, 0, 0)) + ); + }); + it("with negative offset", () => { + expect(_parseRfc3339DateTimeWithOffset("2019-12-16T22:48:18-01:02")).toEqual( + new Date(Date.UTC(2019, 11, 16, 23, 50, 18, 0)) + ); + }); + it("with positive offset", () => { + expect(_parseRfc3339DateTimeWithOffset("2019-12-16T22:48:18+02:04")).toEqual( + new Date(Date.UTC(2019, 11, 16, 20, 44, 18, 0)) + ); + }); + }); + + it.each(invalidRfc3339DateTimes)("rejects %s", (value) => { + expect(() => _parseRfc3339DateTimeWithOffset(value)).toThrowError(); + }); +}); + +describe("_parseRfc7231DateTime", () => { + it.each([null, undefined])("returns undefined for %s", (value) => { + expect(_parseRfc7231DateTime(value)).toBeUndefined(); + }); + + describe("parses properly formatted dates", () => { + describe("with fractional seconds", () => { + it.each([ + ["imf-fixdate", "Sun, 06 Nov 1994 08:49:37.52 GMT"], + ["rfc-850", "Sunday, 06-Nov-94 08:49:37.52 GMT"], + ["asctime", "Sun Nov 6 08:49:37.52 1994"], + ])("in format %s", (_, value) => { + expect(_parseRfc7231DateTime(value)).toEqual(new Date(Date.UTC(1994, 10, 6, 8, 49, 37, 520))); + }); + }); + describe("with fractional seconds - single digit hour", () => { + it.each([ + ["imf-fixdate", "Sun, 06 Nov 1994 8:49:37.52 GMT"], + ["rfc-850", "Sunday, 06-Nov-94 8:49:37.52 GMT"], + ["asctime", "Sun Nov 6 8:49:37.52 1994"], + ])("in format %s", (_, value) => { + expect(_parseRfc7231DateTime(value)).toEqual(new Date(Date.UTC(1994, 10, 6, 8, 49, 37, 520))); + }); + }); + describe("without fractional seconds", () => { + it.each([ + ["imf-fixdate", "Sun, 06 Nov 1994 08:49:37 GMT"], + ["rfc-850", "Sunday, 06-Nov-94 08:49:37 GMT"], + ["asctime", "Sun Nov 6 08:49:37 1994"], + ])("in format %s", (_, value) => { + expect(_parseRfc7231DateTime(value)).toEqual(new Date(Date.UTC(1994, 10, 6, 8, 49, 37, 0))); + }); + }); + describe("without fractional seconds - single digit hour", () => { + it.each([ + ["imf-fixdate", "Sun, 06 Nov 1994 8:49:37 GMT"], + ["rfc-850", "Sunday, 06-Nov-94 8:49:37 GMT"], + ["asctime", "Sun Nov 6 8:49:37 1994"], + ])("in format %s", (_, value) => { + expect(_parseRfc7231DateTime(value)).toEqual(new Date(Date.UTC(1994, 10, 6, 8, 49, 37, 0))); + }); + }); + describe("with leap seconds", () => { + it.each([ + ["imf-fixdate", "Mon, 31 Dec 1990 15:59:60 GMT"], + ["rfc-850", "Monday, 31-Dec-90 15:59:60 GMT"], + ["asctime", "Mon Dec 31 15:59:60 1990"], + ])("in format %s", (_, value) => { + expect(_parseRfc7231DateTime(value)).toEqual(new Date(Date.UTC(1990, 11, 31, 15, 59, 60, 0))); + }); + }); + describe("with leap seconds - single digit hour", () => { + it.each([ + ["imf-fixdate", "Mon, 31 Dec 1990 8:59:60 GMT"], + ["rfc-850", "Monday, 31-Dec-90 8:59:60 GMT"], + ["asctime", "Mon Dec 31 8:59:60 1990"], + ])("in format %s", (_, value) => { + expect(_parseRfc7231DateTime(value)).toEqual(new Date(Date.UTC(1990, 11, 31, 8, 59, 60, 0))); + }); + }); + describe("with leap days", () => { + it.each([ + ["imf-fixdate", "Sun, 29 Feb 2004 15:59:59 GMT"], + ["asctime", "Sun Feb 29 15:59:59 2004"], + ])("in format %s", (_, value) => { + expect(_parseRfc7231DateTime(value)).toEqual(new Date(Date.UTC(2004, 1, 29, 15, 59, 59, 0))); + }); + }); + describe("with leap days - single digit hour", () => { + it.each([ + ["imf-fixdate", "Sun, 29 Feb 2004 8:59:59 GMT"], + ["asctime", "Sun Feb 29 8:59:59 2004"], + ])("in format %s", (_, value) => { + expect(_parseRfc7231DateTime(value)).toEqual(new Date(Date.UTC(2004, 1, 29, 8, 59, 59, 0))); + }); + }); + describe("with leading zeroes", () => { + it.each([ + ["imf-fixdate", "Sun, 06 Nov 0104 08:09:07.02 GMT", 104], + ["rfc-850", "Sunday, 06-Nov-04 08:09:07.02 GMT", 1904], + ["asctime", "Sun Nov 6 08:09:07.02 0104", 104], + ])("in format %s", (_, value, year) => { + expect(_parseRfc7231DateTime(value)).toEqual(new Date(Date.UTC(year, 10, 6, 8, 9, 7, 20))); + }); + }); + describe("with all-zero components", () => { + it.each([ + ["imf-fixdate", "Sun, 06 Nov 0104 00:00:00.00 GMT", 104], + ["rfc-850", "Sunday, 06-Nov-94 00:00:00.00 GMT", 1994], + ["asctime", "Sun Nov 6 00:00:00.00 0104", 104], + ])("in format %s", (_, value, year) => { + expect(_parseRfc7231DateTime(value)).toEqual(new Date(Date.UTC(year, 10, 6, 0, 0, 0, 0))); + }); + }); + }); + + // note: some edge cases are not handled because the amount of code needed to enforce + // them client-side is excessive. We trust our services' response values. + it.each([ + "1985-04-12T23:20:50.52Z", // RFC 3339 format, not RFC 7231 + "1985-04-12T23:20:50Z", // RFC 3339 format, not RFC 7231 + + "Sun, 06 Nov 0004 08:09:07.02 GMTSun, 06 Nov 0004 08:09:07.02 GMT", // Contains multiple timestamps + "Sun, 06 Nov 0004 08:09:07.02 GMTA", // Contains invalid characters after GMT + "ASun, 06 Nov 0004 08:09:07.02 GMT", // Contains invalid characters before timestamp + "Sun, 06 Nov 94 08:49:37 GMT", // Year must be 4 digits + "Sun, 06 Dov 1994 08:49:37 GMT", // Invalid month name + "Mun, 06 Nov 1994 08:49:37 GMT", // Invalid day name + // "Sunday, 06 Nov 1994 08:49:37 GMT", // Wrong format - uses full day name in IMF-fixdate format + "Sun, 06 November 1994 08:49:37 GMT", // Wrong format - uses full month name + "Sun, 06 Nov 1994 24:49:37 GMT", // Hours cannot be 24 + "Sun, 06 Nov 1994 08:69:37 GMT", // Minutes cannot be > 59 + "Sun, 06 Nov 1994 08:49:67 GMT", // Seconds cannot be > 60 (60 only allowed for leap second) + "Sun, 06-11-1994 08:49:37 GMT", // Wrong date format - uses dashes instead of spaces + "Sun, 06 11 1994 08:49:37 GMT", // Wrong format - uses numeric month instead of abbreviated name + // "Sun, 31 Nov 1994 08:49:37 GMT", // Invalid date - November has 30 days + // "Sun, 29 Feb 2005 15:59:59 GMT", // Invalid date - 2005 was not a leap year + + "Sunday, 06-Nov-04 08:09:07.02 GMTSunday, 06-Nov-04 08:09:07.02 GMT", // Contains multiple timestamps + "ASunday, 06-Nov-04 08:09:07.02 GMT", // Contains invalid characters before timestamp + "Sunday, 06-Nov-04 08:09:07.02 GMTA", // Contains invalid characters after GMT + "Sunday, 06-Nov-1994 08:49:37 GMT", // Wrong format - uses 4 digit year in RFC 850 format + "Sunday, 06-Dov-94 08:49:37 GMT", // Invalid month name + "Sundae, 06-Nov-94 08:49:37 GMT", // Invalid day name + // "Sun, 06-Nov-94 08:49:37 GMT", // Wrong format - uses abbreviated day name in RFC 850 format + "Sunday, 06-November-94 08:49:37 GMT", // Wrong format - uses full month name + "Sunday, 06-Nov-94 24:49:37 GMT", // Hours cannot be 24 + "Sunday, 06-Nov-94 08:69:37 GMT", // Minutes cannot be > 59 + "Sunday, 06-Nov-94 08:49:67 GMT", // Seconds cannot be > 60 (60 only allowed for leap second) + "Sunday, 06 11 94 08:49:37 GMT", // Wrong format - uses spaces instead of dashes + "Sunday, 06-11-1994 08:49:37 GMT", // Wrong format - uses numeric month and 4 digit year + // "Sunday, 31-Nov-94 08:49:37 GMT", // Invalid date - November has 30 days + // "Sunday, 29-Feb-05 15:59:59 GMT", // Invalid date - 2005 was not a leap year + + "Sun Nov 6 08:09:07.02 0004Sun Nov 6 08:09:07.02 0004", // Contains multiple timestamps + "ASun Nov 6 08:09:07.02 0004", // Contains invalid characters before timestamp + "Sun Nov 6 08:09:07.02 0004A", // Contains invalid characters after timestamp + "Sun Nov 6 08:49:37 94", // Year must be 4 digits in asctime format + "Sun Dov 6 08:49:37 1994", // Invalid month name + "Mun Nov 6 08:49:37 1994", // Invalid day name + // "Sunday Nov 6 08:49:37 1994", // Wrong format - uses full day name + "Sun November 6 08:49:37 1994", // Wrong format - uses full month name + "Sun Nov 6 24:49:37 1994", // Hours cannot be 24 + "Sun Nov 6 08:69:37 1994", // Minutes cannot be > 59 + "Sun Nov 6 08:49:67 1994", // Seconds cannot be > 60 (60 only allowed for leap second) + "Sun 06-11 08:49:37 1994", // Wrong format - uses dashes in date + "Sun 06 11 08:49:37 1994", // Wrong format - uses numeric month + "Sun 11 6 08:49:37 1994", // Wrong format - month and day in wrong order + // "Sun Nov 31 08:49:37 1994", // Invalid date - November has 30 days + // "Sun Feb 29 15:59:59 2005", // Invalid date - 2005 was not a leap year + "Sun Nov 6 08:49:37 1994", // Wrong format - missing space after single digit day + ])("rejects %s", (value) => { + expect(() => _parseRfc7231DateTime(value)).toThrowError(); + }); +}); + +describe("_parseEpochTimestamp", () => { + it.each([null, undefined])("returns undefined for %s", (value) => { + expect(_parseEpochTimestamp(value)).toBeUndefined(); + }); + + describe("parses properly formatted dates", () => { + describe("with fractional seconds", () => { + it.each(["482196050.52", 482196050.52])("parses %s", (value) => { + expect(_parseEpochTimestamp(value)).toEqual(new Date(Date.UTC(1985, 3, 12, 23, 20, 50, 520))); + }); + }); + describe("without fractional seconds", () => { + it.each(["482196050", 482196050, 482196050.0])("parses %s", (value) => { + expect(_parseEpochTimestamp(value)).toEqual(new Date(Date.UTC(1985, 3, 12, 23, 20, 50, 0))); + }); + }); + }); + it.each([ + "1985-04-12T23:20:50.52Z", + "1985-04-12T23:20:50Z", + "Mon, 31 Dec 1990 15:59:60 GMT", + "Monday, 31-Dec-90 15:59:60 GMT", + "Mon Dec 31 15:59:60 1990", + "NaN", + NaN, + "Infinity", + Infinity, + "0x42", + ])("rejects %s", (value) => { + expect(() => _parseEpochTimestamp(value)).toThrowError(); + }); +}); diff --git a/packages/core/src/submodules/serde/schema-serde-lib/schema-date-utils.ts b/packages/core/src/submodules/serde/schema-serde-lib/schema-date-utils.ts new file mode 100644 index 00000000000..a19ddd23c90 --- /dev/null +++ b/packages/core/src/submodules/serde/schema-serde-lib/schema-date-utils.ts @@ -0,0 +1,176 @@ +const ddd = `(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)(?:[ne|u?r]?s?day)?`; +const mmm = `(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)`; +const time = `(\\d?\\d):(\\d{2}):(\\d{2})(?:\\.(\\d+))?`; +const date = `(\\d?\\d)`; +const year = `(\\d{4})`; + +const RFC3339_WITH_OFFSET = new RegExp( + /^(\d{4})-(\d\d)-(\d\d)[tT](\d\d):(\d\d):(\d\d)(\.(\d+))?(([-+]\d\d:\d\d)|[zZ])$/ +); +const IMF_FIXDATE = new RegExp(`^${ddd}, ${date} ${mmm} ${year} ${time} GMT$`); +const RFC_850_DATE = new RegExp(`^${ddd}, ${date}-${mmm}-(\\d\\d) ${time} GMT$`); +const ASC_TIME = new RegExp(`^${ddd} ${mmm} ( [1-9]|\\d\\d) ${time} ${year}$`); + +const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + +/** + * @internal + * + * Parses a value into a Date. Returns undefined if the input is null or + * undefined, throws an error if the input is not a number or a parseable string. + * + * Input strings must be an integer or floating point number. Fractional seconds are supported. + * + * @param value - the value to parse + * @returns a Date or undefined + */ +export const _parseEpochTimestamp = (value: unknown): Date | undefined => { + if (value == null) { + return void 0; + } + let num: number = NaN; + if (typeof value === "number") { + num = value; + } else if (typeof value === "string") { + if (!/^-?\d*\.?\d+$/.test(value)) { + throw new TypeError(`parseEpochTimestamp - numeric string invalid.`); + } + num = Number.parseFloat(value); + } else if (typeof value === "object" && (value as { tag: number; value: number }).tag === 1) { + // timestamp is a CBOR tag type. + num = (value as { tag: number; value: number }).value; + } + if (isNaN(num) || Math.abs(num) === Infinity) { + throw new TypeError("Epoch timestamps must be valid finite numbers."); + } + return new Date(Math.round(num * 1000)); +}; + +/** + * @internal + * + * Parses a value into a Date. Returns undefined if the input is null or + * undefined, throws an error if the input is not a string that can be parsed + * as an RFC 3339 date. + * + * Input strings must conform to RFC3339 section 5.6, and can have a UTC + * offset. Fractional precision is supported. + * + * @see {@link https://xml2rfc.tools.ietf.org/public/rfc/html/rfc3339.html#anchor14} + * + * @param value - the value to parse + * @returns a Date or undefined + */ +export const _parseRfc3339DateTimeWithOffset = (value: unknown): Date | undefined => { + if (value == null) { + return void 0; + } + if (typeof value !== "string") { + throw new TypeError("RFC3339 timestamps must be strings"); + } + + const matches = RFC3339_WITH_OFFSET.exec(value); + + if (!matches) { + throw new TypeError(`Invalid RFC3339 timestamp format ${value}`); + } + + const [, yearStr, monthStr, dayStr, hours, minutes, seconds, , ms, offsetStr] = matches; + + range(monthStr, 1, 12); + range(dayStr, 1, 31); + range(hours, 0, 23); + range(minutes, 0, 59); + range(seconds, 0, 60); // leap second + + const date = new Date(); + date.setUTCFullYear(Number(yearStr), Number(monthStr) - 1, Number(dayStr)); + date.setUTCHours(Number(hours)); + date.setUTCMinutes(Number(minutes)); + date.setUTCSeconds(Number(seconds)); + date.setUTCMilliseconds(Number(ms) ? Math.round(parseFloat(`0.${ms}`) * 1000) : 0); + + if (offsetStr.toUpperCase() != "Z") { + const [, sign, offsetH, offsetM] = /([+-])(\d\d):(\d\d)/.exec(offsetStr) || [void 0, "+", 0, 0]; + const scalar = sign === "-" ? 1 : -1; + date.setTime(date.getTime() + scalar * (Number(offsetH) * 60 * 60 * 1000 + Number(offsetM) * 60 * 1000)); + } + + return date; +}; + +/** + * @internal + * + * Parses a value into a Date. Returns undefined if the input is null or + * undefined, throws an error if the input is not a string that can be parsed + * as an RFC 7231 date. + * + * Input strings must conform to RFC7231 section 7.1.1.1. Fractional seconds are supported. + * + * RFC 850 and unix asctime formats are also accepted. + * todo: practically speaking, are RFC 850 and asctime even used anymore? + * todo: can we remove those parts? + * + * @see {@link https://datatracker.ietf.org/doc/html/rfc7231.html#section-7.1.1.1} + * + * @param value - the value to parse. + * @returns a Date or undefined. + */ +export const _parseRfc7231DateTime = (value: unknown): Date | undefined => { + if (value == null) { + return void 0; + } + if (typeof value !== "string") { + throw new TypeError("RFC7231 timestamps must be strings."); + } + + let day!: string; + let month!: string; + let year!: string; + let hour!: string; + let minute!: string; + let second!: string; + let fraction!: string; + + let matches: string[] | null; + if ((matches = IMF_FIXDATE.exec(value))) { + // "Mon, 25 Dec 2077 23:59:59 GMT" + [, day, month, year, hour, minute, second, fraction] = matches; + } else if ((matches = RFC_850_DATE.exec(value))) { + // "Monday, 25-Dec-77 23:59:59 GMT" + [, day, month, year, hour, minute, second, fraction] = matches; + year = (Number(year) + 1900).toString(); + } else if ((matches = ASC_TIME.exec(value))) { + // "Mon Dec 25 23:59:59 2077" + [, month, day, hour, minute, second, fraction, year] = matches; + } + + if (year && second) { + const date = new Date(); + date.setUTCFullYear(Number(year)); + date.setUTCMonth(months.indexOf(month)); + range(day, 1, 31); + date.setUTCDate(Number(day)); + range(hour, 0, 23); + date.setUTCHours(Number(hour)); + range(minute, 0, 59); + date.setUTCMinutes(Number(minute)); + range(second, 0, 60); // leap second. + date.setUTCSeconds(Number(second)); + date.setUTCMilliseconds(fraction ? Math.round(parseFloat(`0.${fraction}`) * 1000) : 0); + return date; + } + + throw new TypeError(`Invalid RFC7231 date-time value ${value}.`); +}; + +/** + * @internal + */ +function range(v: number | string, min: number, max: number): void { + const _v = Number(v); + if (_v < min || _v > max) { + throw new Error(`Value ${_v} out of range [${min}, ${max}]`); + } +} diff --git a/packages/core/src/submodules/serde/value/NumericValue.spec.ts b/packages/core/src/submodules/serde/value/NumericValue.spec.ts index f790632ba7f..3eb1ed89351 100644 --- a/packages/core/src/submodules/serde/value/NumericValue.spec.ts +++ b/packages/core/src/submodules/serde/value/NumericValue.spec.ts @@ -24,11 +24,8 @@ describe(NumericValue.name, () => { new NumericValue("0", "bigDecimal"), new NumericValue("-0.00", "bigDecimal"), { - string: "abcd", + string: "-.123", type: "bigDecimal", - constructor: { - name: "_NumericValue", - }, }, (() => { const x = {}; @@ -52,9 +49,6 @@ describe(NumericValue.name, () => { { string: "abcd", type: "bigDecimal", - constructor: { - name: "_NumericValue_", - }, }, (() => { const x = {}; diff --git a/packages/core/src/submodules/serde/value/NumericValue.ts b/packages/core/src/submodules/serde/value/NumericValue.ts index d3b081ca42a..9900b2ece08 100644 --- a/packages/core/src/submodules/serde/value/NumericValue.ts +++ b/packages/core/src/submodules/serde/value/NumericValue.ts @@ -8,6 +8,11 @@ */ export type NumericType = "bigDecimal"; +/** + * @internal + */ +const format = /^-?\d*(\.\d+)?$/; + /** * Serialization container for Smithy simple types that do not have a * direct JavaScript runtime representation. @@ -25,27 +30,10 @@ export class NumericValue { public readonly string: string, public readonly type: NumericType ) { - let dot = 0; - for (let i = 0; i < string.length; ++i) { - const char = string.charCodeAt(i); - if (i === 0 && char === 45) { - // negation prefix "-" - continue; - } - if (char === 46) { - // decimal point "." - if (dot) { - throw new Error("@smithy/core/serde - NumericValue must contain at most one decimal point."); - } - dot = 1; - continue; - } - if (char < 48 || char > 57) { - // not in 0 through 9 - throw new Error( - `@smithy/core/serde - NumericValue must only contain [0-9], at most one decimal point ".", and an optional negation prefix "-".` - ); - } + if (!format.test(string)) { + throw new Error( + `@smithy/core/serde - NumericValue must only contain [0-9], at most one decimal point ".", and an optional negation prefix "-".` + ); } } @@ -58,18 +46,7 @@ export class NumericValue { return false; } const _nv = object as NumericValue; - const prototypeMatch = NumericValue.prototype.isPrototypeOf(object); - if (prototypeMatch) { - return prototypeMatch; - } - if ( - typeof _nv.string === "string" && - typeof _nv.type === "string" && - _nv.constructor?.name?.endsWith("NumericValue") - ) { - return true; - } - return prototypeMatch; + return NumericValue.prototype.isPrototypeOf(object) || (_nv.type === "bigDecimal" && format.test(_nv.string)); } } diff --git a/packages/util-base64/src/constants.browser.ts b/packages/util-base64/src/constants.browser.ts index 256d216ef7b..76a45aa6ef0 100644 --- a/packages/util-base64/src/constants.browser.ts +++ b/packages/util-base64/src/constants.browser.ts @@ -1,34 +1,14 @@ -const alphabetByEncoding: Record = {}; -const alphabetByValue: Array = new Array(64); - -for (let i = 0, start = "A".charCodeAt(0), limit = "Z".charCodeAt(0); i + start <= limit; i++) { - const char = String.fromCharCode(i + start); - alphabetByEncoding[char] = i; - alphabetByValue[i] = char; -} - -for (let i = 0, start = "a".charCodeAt(0), limit = "z".charCodeAt(0); i + start <= limit; i++) { - const char = String.fromCharCode(i + start); - const index = i + 26; - alphabetByEncoding[char] = index; - alphabetByValue[index] = char; -} - -for (let i = 0; i < 10; i++) { - alphabetByEncoding[i.toString(10)] = i + 52; - const char = i.toString(10); - const index = i + 52; - alphabetByEncoding[char] = index; - alphabetByValue[index] = char; -} - -alphabetByEncoding["+"] = 62; -alphabetByValue[62] = "+"; -alphabetByEncoding["/"] = 63; -alphabetByValue[63] = "/"; - -const bitsPerLetter = 6; -const bitsPerByte = 8; -const maxLetterValue = 0b111111; - -export { alphabetByEncoding, alphabetByValue, bitsPerLetter, bitsPerByte, maxLetterValue }; +const chars = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`; + +export const alphabetByEncoding: Record = Object.entries(chars).reduce( + (acc, [i, c]) => { + acc[c] = Number(i); + return acc; + }, + {} as Record +); +export const alphabetByValue: Array = chars.split(""); + +export const bitsPerLetter = 6; +export const bitsPerByte = 8; +export const maxLetterValue = 0b111111; diff --git a/packages/util-base64/vitest.config.mts b/packages/util-base64/vitest.config.mts index 4e46707824a..0354cc268ab 100644 --- a/packages/util-base64/vitest.config.mts +++ b/packages/util-base64/vitest.config.mts @@ -2,7 +2,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - exclude: ["**/*.{integ,e2e,browser}.spec.ts"], + exclude: ["**/*.{integ,e2e}.spec.ts"], include: ["**/*.spec.ts"], environment: "node", }, diff --git a/packages/util-stream/src/blob/Uint8ArrayBlobAdapter.ts b/packages/util-stream/src/blob/Uint8ArrayBlobAdapter.ts index 447be19b21d..2a78e35906b 100644 --- a/packages/util-stream/src/blob/Uint8ArrayBlobAdapter.ts +++ b/packages/util-stream/src/blob/Uint8ArrayBlobAdapter.ts @@ -7,15 +7,14 @@ import { transformFromString, transformToString } from "./transforms"; export class Uint8ArrayBlobAdapter extends Uint8Array { /** * @param source - such as a string or Stream. + * @param encoding - utf-8 or base64. * @returns a new Uint8ArrayBlobAdapter extending Uint8Array. */ public static fromString(source: string, encoding = "utf-8"): Uint8ArrayBlobAdapter { - switch (typeof source) { - case "string": - return transformFromString(source, encoding); - default: - throw new Error(`Unsupported conversion from ${typeof source} to Uint8ArrayBlobAdapter.`); + if (typeof source === "string") { + return transformFromString(source, encoding); } + throw new Error(`Unsupported conversion from ${typeof source} to Uint8ArrayBlobAdapter.`); } /**