diff --git a/.changeset/pink-items-shake.md b/.changeset/pink-items-shake.md new file mode 100644 index 00000000000..b7192e678f8 --- /dev/null +++ b/.changeset/pink-items-shake.md @@ -0,0 +1,6 @@ +--- +"@smithy/types": minor +"@smithy/core": minor +--- + +schema code size optimizations diff --git a/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.spec.ts b/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.spec.ts index 37eca9a4c63..f1d64865c09 100644 --- a/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.spec.ts +++ b/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.spec.ts @@ -153,6 +153,7 @@ describe(SmithyRpcV2CborProtocol.name, () => { const protocol = new SmithyRpcV2CborProtocol({ defaultNamespace: "" }); const httpRequest = await protocol.serializeRequest( { + namespace: "ns", name: "dummy", input: testCase.schema, output: "unit", @@ -256,6 +257,7 @@ describe(SmithyRpcV2CborProtocol.name, () => { }); const output = await protocol.deserializeResponse( { + namespace: "ns", name: "dummy", input: "unit", output: testCase.schema, diff --git a/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.ts b/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.ts index 8b83922109f..108a1671655 100644 --- a/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.ts +++ b/packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.ts @@ -1,5 +1,5 @@ import { RpcProtocol } from "@smithy/core/protocols"; -import type { ErrorSchema, OperationSchema } from "@smithy/core/schema"; +import type { ErrorSchema } from "@smithy/core/schema"; import { deref, NormalizedSchema, TypeRegistry } from "@smithy/core/schema"; import type { EndpointBearer, @@ -7,6 +7,7 @@ import type { HttpRequest as IHttpRequest, HttpResponse as IHttpResponse, MetadataBearer, + OperationSchema, ResponseMetadata, SerdeFunctions, } from "@smithy/types"; diff --git a/packages/core/src/submodules/protocols/HttpBindingProtocol.spec.ts b/packages/core/src/submodules/protocols/HttpBindingProtocol.spec.ts index 95c1f9437e6..98eba8f158f 100644 --- a/packages/core/src/submodules/protocols/HttpBindingProtocol.spec.ts +++ b/packages/core/src/submodules/protocols/HttpBindingProtocol.spec.ts @@ -64,7 +64,7 @@ describe(HttpBindingProtocol.name, () => { const response = new HttpResponse({ statusCode: 200, headers: { - "x-timestamplist": "Mon, 16 Dec 2019 23:48:18 GMT, Mon, 16 Dec 2019 23:48:18 GMT", + "x-timestamplist": "Mon, 16 Nov 2019 23:48:18 GMT, Mon, 16 Dec 2019 23:48:18 GMT", }, }); @@ -95,7 +95,7 @@ describe(HttpBindingProtocol.name, () => { )) as Partial; delete output.$metadata; expect(output).toEqual({ - timestampList: [new Date("2019-12-16T23:48:18.000Z"), new Date("2019-12-16T23:48:18.000Z")], + timestampList: [new Date("2019-11-16T23:48:18.000Z"), new Date("2019-12-16T23:48:18.000Z")], }); }); diff --git a/packages/core/src/submodules/protocols/HttpBindingProtocol.ts b/packages/core/src/submodules/protocols/HttpBindingProtocol.ts index 0feddbf7745..5f38158f92f 100644 --- a/packages/core/src/submodules/protocols/HttpBindingProtocol.ts +++ b/packages/core/src/submodules/protocols/HttpBindingProtocol.ts @@ -157,20 +157,15 @@ export abstract class HttpBindingProtocol extends HttpProtocol { if (traits.httpQueryParams) { for (const [key, val] of Object.entries(data)) { if (!(key in query)) { - this.serializeQuery( - NormalizedSchema.of([ - ns.getValueSchema(), - { - // We pass on the traits to the sub-schema - // because we are still in the process of serializing the map itself. - ...traits, - httpQuery: key, - httpQueryParams: undefined, - }, - ]), - val, - query - ); + const valueSchema = ns.getValueSchema(); + Object.assign(valueSchema.getMergedTraits(), { + // We pass on the traits to the sub-schema + // because we are still in the process of serializing the map itself. + ...traits, + httpQuery: key, + httpQueryParams: undefined, + }); + this.serializeQuery(valueSchema, val, query); } } return; @@ -305,6 +300,7 @@ export abstract class HttpBindingProtocol extends HttpProtocol { if (null != value) { if (memberSchema.isListSchema()) { const headerListValueSchema = memberSchema.getValueSchema(); + headerListValueSchema.getMergedTraits().httpHeader = key; let sections: string[]; if ( headerListValueSchema.isTimestampSchema() && @@ -316,7 +312,7 @@ export abstract class HttpBindingProtocol extends HttpProtocol { } const list = []; for (const section of sections) { - list.push(await deserializer.read([headerListValueSchema, { httpHeader: key }], section.trim())); + list.push(await deserializer.read(headerListValueSchema, section.trim())); } dataObject[memberName] = list; } else { @@ -327,8 +323,10 @@ export abstract class HttpBindingProtocol extends HttpProtocol { dataObject[memberName] = {}; for (const [header, value] of Object.entries(response.headers)) { if (header.startsWith(memberTraits.httpPrefixHeaders)) { + const valueSchema = memberSchema.getValueSchema(); + valueSchema.getMergedTraits().httpHeader = header; dataObject[memberName][header.slice(memberTraits.httpPrefixHeaders.length)] = await deserializer.read( - [memberSchema.getValueSchema(), { httpHeader: header }], + valueSchema, value ); } diff --git a/packages/core/src/submodules/protocols/serde/FromStringShapeDeserializer.ts b/packages/core/src/submodules/protocols/serde/FromStringShapeDeserializer.ts index a92def28c31..8c1f1bd47cd 100644 --- a/packages/core/src/submodules/protocols/serde/FromStringShapeDeserializer.ts +++ b/packages/core/src/submodules/protocols/serde/FromStringShapeDeserializer.ts @@ -29,14 +29,18 @@ export class FromStringShapeDeserializer implements ShapeDeserializer { public read(_schema: Schema, data: string): any { const ns = NormalizedSchema.of(_schema); + if (ns.isListSchema()) { return splitHeader(data).map((item) => this.read(ns.getValueSchema(), item)); } + if (ns.isBlobSchema()) { return (this.serdeContext?.base64Decoder ?? fromBase64)(data); } + if (ns.isTimestampSchema()) { const format = determineTimestampFormat(ns, this.settings); + switch (format) { case SCHEMA.TIMESTAMP_DATE_TIME: return parseRfc3339DateTimeWithOffset(data); diff --git a/packages/core/src/submodules/protocols/serde/determineTimestampFormat.ts b/packages/core/src/submodules/protocols/serde/determineTimestampFormat.ts index ce05f1ff82c..1e9d48b39ae 100644 --- a/packages/core/src/submodules/protocols/serde/determineTimestampFormat.ts +++ b/packages/core/src/submodules/protocols/serde/determineTimestampFormat.ts @@ -29,6 +29,7 @@ export function determineTimestampFormat( } const { httpLabel, httpPrefixHeaders, httpHeader, httpQuery } = ns.getMergedTraits(); + const bindingFormat = settings.httpBindings ? typeof httpPrefixHeaders === "string" || Boolean(httpHeader) ? SCHEMA.TIMESTAMP_HTTP_DATE diff --git a/packages/core/src/submodules/schema/schemas/ErrorSchema.ts b/packages/core/src/submodules/schema/schemas/ErrorSchema.ts index 37db0a72cd2..cf6d441b7c0 100644 --- a/packages/core/src/submodules/schema/schemas/ErrorSchema.ts +++ b/packages/core/src/submodules/schema/schemas/ErrorSchema.ts @@ -1,6 +1,7 @@ import type { SchemaRef, SchemaTraits } from "@smithy/types"; import { TypeRegistry } from "../TypeRegistry"; +import { Schema } from "./Schema"; import { StructureSchema } from "./StructureSchema"; /** @@ -12,30 +13,9 @@ import { StructureSchema } from "./StructureSchema"; * @alpha */ export class ErrorSchema extends StructureSchema { - public static symbol = Symbol.for("@smithy/core/schema::ErrorSchema"); - protected symbol = ErrorSchema.symbol; - - public constructor( - public name: string, - public traits: SchemaTraits, - public memberNames: string[], - public memberList: SchemaRef[], - /** - * Constructor for a modeled service exception class that extends Error. - */ - public ctor: any - ) { - super(name, traits, memberNames, memberList); - } - - public static [Symbol.hasInstance](lhs: unknown): lhs is ErrorSchema { - const isPrototype = ErrorSchema.prototype.isPrototypeOf(lhs as any); - if (!isPrototype && typeof lhs === "object" && lhs !== null) { - const err = lhs as ErrorSchema; - return err.symbol === ErrorSchema.symbol; - } - return isPrototype; - } + public static readonly symbol = Symbol.for("@smithy/err"); + public ctor!: any; + protected readonly symbol = ErrorSchema.symbol; } /** @@ -50,15 +30,19 @@ export class ErrorSchema extends StructureSchema { * @param memberList - list of schemaRef corresponding to each * @param ctor - class reference for the existing Error extending class. */ -export function error( +export const error = ( namespace: string, name: string, - traits: SchemaTraits = {}, + traits: SchemaTraits, memberNames: string[], memberList: SchemaRef[], ctor: any -): ErrorSchema { - const schema = new ErrorSchema(namespace + "#" + name, traits, memberNames, memberList, ctor); - TypeRegistry.for(namespace).register(name, schema); - return schema; -} +): ErrorSchema => + Schema.assign(new ErrorSchema(), { + name, + namespace, + traits, + memberNames, + memberList, + ctor, + }); diff --git a/packages/core/src/submodules/schema/schemas/ListSchema.ts b/packages/core/src/submodules/schema/schemas/ListSchema.ts index dc14fab0df6..a474e7ac32e 100644 --- a/packages/core/src/submodules/schema/schemas/ListSchema.ts +++ b/packages/core/src/submodules/schema/schemas/ListSchema.ts @@ -1,6 +1,5 @@ import type { ListSchema as IListSchema, SchemaRef, SchemaTraits } from "@smithy/types"; -import { TypeRegistry } from "../TypeRegistry"; import { Schema } from "./Schema"; /** @@ -10,25 +9,11 @@ import { Schema } from "./Schema"; * @alpha */ export class ListSchema extends Schema implements IListSchema { - public static symbol = Symbol.for("@smithy/core/schema::ListSchema"); - protected symbol = ListSchema.symbol; - - public constructor( - public name: string, - public traits: SchemaTraits, - public valueSchema: SchemaRef - ) { - super(name, traits); - } - - public static [Symbol.hasInstance](lhs: unknown): lhs is ListSchema { - const isPrototype = ListSchema.prototype.isPrototypeOf(lhs as any); - if (!isPrototype && typeof lhs === "object" && lhs !== null) { - const list = lhs as ListSchema; - return list.symbol === ListSchema.symbol; - } - return isPrototype; - } + public static readonly symbol = Symbol.for("@smithy/lis"); + public name!: string; + public traits!: SchemaTraits; + public valueSchema!: SchemaRef; + protected readonly symbol = ListSchema.symbol; } /** @@ -36,12 +21,10 @@ export class ListSchema extends Schema implements IListSchema { * * @internal */ -export function list(namespace: string, name: string, traits: SchemaTraits = {}, valueSchema: SchemaRef): ListSchema { - const schema = new ListSchema( - namespace + "#" + name, +export const list = (namespace: string, name: string, traits: SchemaTraits, valueSchema: SchemaRef): ListSchema => + Schema.assign(new ListSchema(), { + name, + namespace, traits, - typeof valueSchema === "function" ? valueSchema() : valueSchema - ); - TypeRegistry.for(namespace).register(name, schema); - return schema; -} + valueSchema, + }); diff --git a/packages/core/src/submodules/schema/schemas/MapSchema.ts b/packages/core/src/submodules/schema/schemas/MapSchema.ts index 301aaf8db56..bfb450845a0 100644 --- a/packages/core/src/submodules/schema/schemas/MapSchema.ts +++ b/packages/core/src/submodules/schema/schemas/MapSchema.ts @@ -1,6 +1,5 @@ import type { MapSchema as IMapSchema, SchemaRef, SchemaTraits } from "@smithy/types"; -import { TypeRegistry } from "../TypeRegistry"; import { Schema } from "./Schema"; /** @@ -8,48 +7,32 @@ import { Schema } from "./Schema"; * @alpha */ export class MapSchema extends Schema implements IMapSchema { - public static symbol = Symbol.for("@smithy/core/schema::MapSchema"); - protected symbol = MapSchema.symbol; - - public constructor( - public name: string, - public traits: SchemaTraits, - /** - * This is expected to be StringSchema, but may have traits. - */ - public keySchema: SchemaRef, - public valueSchema: SchemaRef - ) { - super(name, traits); - } - - public static [Symbol.hasInstance](lhs: unknown): lhs is MapSchema { - const isPrototype = MapSchema.prototype.isPrototypeOf(lhs as any); - if (!isPrototype && typeof lhs === "object" && lhs !== null) { - const map = lhs as MapSchema; - return map.symbol === MapSchema.symbol; - } - return isPrototype; - } + public static readonly symbol = Symbol.for("@smithy/map"); + public name!: string; + public traits!: SchemaTraits; + /** + * This is expected to be StringSchema, but may have traits. + */ + public keySchema!: SchemaRef; + public valueSchema!: SchemaRef; + protected readonly symbol = MapSchema.symbol; } /** * Factory for MapSchema. * @internal */ -export function map( +export const map = ( namespace: string, name: string, - traits: SchemaTraits = {}, + traits: SchemaTraits, keySchema: SchemaRef, valueSchema: SchemaRef -): MapSchema { - const schema = new MapSchema( - namespace + "#" + name, +): MapSchema => + Schema.assign(new MapSchema(), { + name, + namespace, traits, keySchema, - typeof valueSchema === "function" ? valueSchema() : valueSchema - ); - TypeRegistry.for(namespace).register(name, schema); - return schema; -} + valueSchema, + }); diff --git a/packages/core/src/submodules/schema/schemas/NormalizedSchema.spec.ts b/packages/core/src/submodules/schema/schemas/NormalizedSchema.spec.ts index 778798c9674..1c057a72b09 100644 --- a/packages/core/src/submodules/schema/schemas/NormalizedSchema.spec.ts +++ b/packages/core/src/submodules/schema/schemas/NormalizedSchema.spec.ts @@ -1,4 +1,4 @@ -import type { MemberSchema } from "@smithy/types"; +import type { MemberSchema, StructureSchema } from "@smithy/types"; import { describe, expect, test as it } from "vitest"; import { list } from "./ListSchema"; @@ -12,8 +12,8 @@ describe(NormalizedSchema.name, () => { const [List, Map, Struct] = [list("ack", "List", { sparse: 1 }, 0), map("ack", "Map", 0, 0, 1), () => schema]; const schema = struct("ack", "Structure", {}, ["list", "map", "struct"], [List, Map, Struct]); - const ns = new NormalizedSchema(() => schema); - const nsFromIndirect = NormalizedSchema.of([() => ns, 0]); + const ns = NormalizedSchema.of(schema); + const nsFromIndirect = NormalizedSchema.of(() => ns); it("has a static constructor", () => { expect(NormalizedSchema.of(ns)).toBeInstanceOf(NormalizedSchema); @@ -105,7 +105,15 @@ describe(NormalizedSchema.name, () => { 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([ns, { streaming: 1 }]).isStreaming()).toBe(true); + + const structWithStreamingMember = struct( + "ack", + "StructWithStreamingMember", + 0, + ["m"], + [sim("ns", "blob", SCHEMA.BLOB, { streaming: 1 })] + ); + expect(NormalizedSchema.of(structWithStreamingMember).getMemberSchema("m").isStreaming()).toBe(true); }); describe("list member", () => { @@ -184,8 +192,8 @@ describe(NormalizedSchema.name, () => { describe("iteration", () => { it("iterates over member schemas", () => { - const iteration = Array.from(ns.structIterator()); - const entries = Object.entries(ns.getMemberSchemas()); + const iteration = Array.from(ns.structIterator()) as [string, NormalizedSchema][]; + const entries = Object.entries(ns.getMemberSchemas()) as [string, NormalizedSchema][]; for (let i = 0; i < iteration.length; i++) { const [name, schema] = iteration[i]; const [entryName, entrySchema] = entries[i]; @@ -203,7 +211,9 @@ describe(NormalizedSchema.name, () => { describe("traits", () => { const member: MemberSchema = [sim("ack", "SimpleString", 0, { idempotencyToken: 1 }), 0b0000_0001]; - const ns = NormalizedSchema.of(member, "member_name"); + const container: StructureSchema = struct("ack", "Container", 0, ["member_name"], [member, 0]); + + const ns = NormalizedSchema.of(container).getMemberSchema("member_name"); it("has merged traits", () => { expect(ns.getMergedTraits()).toEqual({ diff --git a/packages/core/src/submodules/schema/schemas/NormalizedSchema.ts b/packages/core/src/submodules/schema/schemas/NormalizedSchema.ts index 176a28bbb59..af7e8f86a0b 100644 --- a/packages/core/src/submodules/schema/schemas/NormalizedSchema.ts +++ b/packages/core/src/submodules/schema/schemas/NormalizedSchema.ts @@ -10,6 +10,7 @@ import type { 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 { StructureSchema } from "./StructureSchema"; @@ -21,23 +22,23 @@ import { StructureSchema } from "./StructureSchema"; * @alpha */ export class NormalizedSchema implements INormalizedSchema { - public static symbol = Symbol.for("@smithy/core/schema::NormalizedSchema"); - protected symbol = NormalizedSchema.symbol; + public static readonly symbol = Symbol.for("@smithy/nor"); + protected readonly symbol = NormalizedSchema.symbol; - private readonly name: string; - private readonly traits: SchemaTraits; + private readonly name!: string; + private readonly schema!: Exclude; + private readonly _isMemberSchema: boolean; - private _isMemberSchema: boolean; - private schema: Exclude; - private memberTraits: SchemaTraits; + private readonly traits!: SchemaTraits; + private readonly memberTraits: SchemaTraits; private normalizedTraits?: SchemaTraitsObject; /** * @param ref - a polymorphic SchemaRef to be dereferenced/normalized. * @param memberName - optional memberName if this NormalizedSchema should be considered a member schema. */ - public constructor( - private readonly ref: SchemaRef, + private constructor( + readonly ref: SchemaRef, private memberName?: string ) { const traitStack = [] as SchemaTraits[]; @@ -63,13 +64,9 @@ export class NormalizedSchema implements INormalizedSchema { } if (schema instanceof NormalizedSchema) { - this.name = schema.name; - this.traits = schema.traits; - this._isMemberSchema = schema._isMemberSchema; - this.schema = schema.schema; + Object.assign(this, schema); this.memberTraits = Object.assign({}, schema.getMemberTraits(), this.getMemberTraits()); this.normalizedTraits = void 0; - this.ref = schema.ref; this.memberName = memberName ?? schema.memberName; return; } @@ -83,34 +80,35 @@ export class NormalizedSchema implements INormalizedSchema { } this.name = - (typeof this.schema === "object" ? this.schema?.name : void 0) ?? this.memberName ?? this.getSchemaName(); + (this.schema instanceof Schema ? this.schema.getName?.() : void 0) ?? this.memberName ?? this.getSchemaName(); if (this._isMemberSchema && !memberName) { - throw new Error( - `@smithy/core/schema - NormalizedSchema member schema ${this.getName( - true - )} must initialize with memberName argument.` - ); + throw new Error(`@smithy/core/schema - NormalizedSchema member init ${this.getName(true)} missing member name.`); } } public static [Symbol.hasInstance](lhs: unknown): lhs is NormalizedSchema { - const isPrototype = NormalizedSchema.prototype.isPrototypeOf(lhs as any); - if (!isPrototype && typeof lhs === "object" && lhs !== null) { - const ns = lhs as NormalizedSchema; - return ns.symbol === NormalizedSchema.symbol; - } - return isPrototype; + return Schema[Symbol.hasInstance].bind(this)(lhs); } /** * Static constructor that attempts to avoid wrapping a NormalizedSchema within another. */ - public static of(ref: SchemaRef, memberName?: string): NormalizedSchema { + public static of(ref: SchemaRef): NormalizedSchema { if (ref instanceof NormalizedSchema) { return ref; } - return new NormalizedSchema(ref, memberName); + if (Array.isArray(ref)) { + const [ns, traits] = ref; + if (ns instanceof NormalizedSchema) { + Object.assign(ns.getMergedTraits(), NormalizedSchema.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); } /** @@ -123,51 +121,30 @@ export class NormalizedSchema implements INormalizedSchema { } indicator = indicator | 0; const traits = {} as SchemaTraitsObject; - if ((indicator & 1) === 1) { - traits.httpLabel = 1; - } - if (((indicator >> 1) & 1) === 1) { - traits.idempotent = 1; - } - if (((indicator >> 2) & 1) === 1) { - traits.idempotencyToken = 1; - } - if (((indicator >> 3) & 1) === 1) { - traits.sensitive = 1; - } - if (((indicator >> 4) & 1) === 1) { - traits.httpPayload = 1; - } - if (((indicator >> 5) & 1) === 1) { - traits.httpResponseCode = 1; - } - if (((indicator >> 6) & 1) === 1) { - traits.httpQueryParams = 1; + 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; } - /** - * Creates a normalized member schema from the given schema and member name. - */ - private static memberFrom( - memberSchema: NormalizedSchema | [SchemaRef, SchemaTraits], - memberName: string - ): NormalizedSchema { - if (memberSchema instanceof NormalizedSchema) { - memberSchema.memberName = memberName; - memberSchema._isMemberSchema = true; - return memberSchema; - } - return new NormalizedSchema(memberSchema, memberName); - } - /** * @returns the underlying non-normalized schema. */ public getSchema(): Exclude { if (this.schema instanceof NormalizedSchema) { - return (this.schema = this.schema.getSchema()); + Object.assign(this, { schema: this.schema.getSchema() }); + return this.schema; } if (this.schema instanceof SimpleSchema) { return deref(this.schema.schemaRef) as Exclude; @@ -195,7 +172,7 @@ export class NormalizedSchema implements INormalizedSchema { */ public getMemberName(): string { if (!this.isMemberSchema()) { - throw new Error(`@smithy/core/schema - cannot get member name on non-member schema: ${this.getName(true)}`); + throw new Error(`@smithy/core/schema - non-member schema: ${this.getName(true)}`); } return this.memberName!; } @@ -227,10 +204,6 @@ export class NormalizedSchema implements INormalizedSchema { return inner instanceof MapSchema; } - public isDocumentSchema(): boolean { - return this.getSchema() === SCHEMA.DOCUMENT; - } - public isStructSchema(): boolean { const inner = this.getSchema(); return (inner !== null && typeof inner === "object" && "members" in inner) || inner instanceof StructureSchema; @@ -245,6 +218,10 @@ export class NormalizedSchema implements INormalizedSchema { return typeof schema === "number" && schema >= SCHEMA.TIMESTAMP_DEFAULT && schema <= SCHEMA.TIMESTAMP_EPOCH_SECONDS; } + public isDocumentSchema(): boolean { + return this.getSchema() === SCHEMA.DOCUMENT; + } + public isStringSchema(): boolean { return this.getSchema() === SCHEMA.STRING; } @@ -291,14 +268,13 @@ export class NormalizedSchema implements INormalizedSchema { * This method is cached. */ public getMergedTraits(): SchemaTraitsObject { - if (this.normalizedTraits) { - return this.normalizedTraits; - } - this.normalizedTraits = { - ...this.getOwnTraits(), - ...this.getMemberTraits(), - }; - return this.normalizedTraits; + return ( + this.normalizedTraits ?? + (this.normalizedTraits = { + ...this.getOwnTraits(), + ...this.getMemberTraits(), + }) + ); } /** @@ -323,16 +299,16 @@ export class NormalizedSchema implements INormalizedSchema { */ public getKeySchema(): NormalizedSchema { if (this.isDocumentSchema()) { - return NormalizedSchema.memberFrom([SCHEMA.DOCUMENT, 0], "key"); + return this.memberFrom([SCHEMA.DOCUMENT, 0], "key"); } if (!this.isMapSchema()) { - throw new Error(`@smithy/core/schema - cannot get key schema for non-map schema: ${this.getName(true)}`); + throw new Error(`@smithy/core/schema - cannot get key for non-map: ${this.getName(true)}`); } const schema = this.getSchema(); if (typeof schema === "number") { - return NormalizedSchema.memberFrom([0b0011_1111 & schema, 0], "key"); + return this.memberFrom([0b0011_1111 & schema, 0], "key"); } - return NormalizedSchema.memberFrom([(schema as MapSchema).keySchema, 0], "key"); + return this.memberFrom([(schema as MapSchema).keySchema, 0], "key"); } /** @@ -346,31 +322,31 @@ export class NormalizedSchema implements INormalizedSchema { if (typeof schema === "number") { if (this.isMapSchema()) { - return NormalizedSchema.memberFrom([0b0011_1111 & schema, 0], "value"); + return this.memberFrom([0b0011_1111 & schema, 0], "value"); } else if (this.isListSchema()) { - return NormalizedSchema.memberFrom([0b0011_1111 & schema, 0], "member"); + return this.memberFrom([0b0011_1111 & schema, 0], "member"); } } if (schema && typeof schema === "object") { if (this.isStructSchema()) { - throw new Error(`cannot call getValueSchema() with StructureSchema ${this.getName(true)}`); + 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 NormalizedSchema.memberFrom([collection.valueSchema, 0], "value"); + return this.memberFrom([collection.valueSchema, 0], "value"); } else if (this.isListSchema()) { - return NormalizedSchema.memberFrom([collection.valueSchema, 0], "member"); + return this.memberFrom([collection.valueSchema, 0], "member"); } } } if (this.isDocumentSchema()) { - return NormalizedSchema.memberFrom([SCHEMA.DOCUMENT, 0], "value"); + return this.memberFrom([SCHEMA.DOCUMENT, 0], "value"); } - throw new Error(`@smithy/core/schema - the schema ${this.getName(true)} does not have a value member.`); + throw new Error(`@smithy/core/schema - ${this.getName(true)} has no value member.`); } /** @@ -380,7 +356,7 @@ export class NormalizedSchema implements INormalizedSchema { public hasMemberSchema(member: string): boolean { if (this.isStructSchema()) { const struct = this.getSchema() as StructureSchema; - return member in struct.members; + return struct.memberNames.includes(member); } return false; } @@ -396,17 +372,17 @@ export class NormalizedSchema implements INormalizedSchema { public getMemberSchema(member: string): NormalizedSchema { if (this.isStructSchema()) { const struct = this.getSchema() as StructureSchema; - if (!(member in struct.members)) { - throw new Error( - `@smithy/core/schema - the schema ${this.getName(true)} does not have a member with name=${member}.` - ); + if (!struct.memberNames.includes(member)) { + throw new Error(`@smithy/core/schema - ${this.getName(true)} has no member=${member}.`); } - return NormalizedSchema.memberFrom(struct.members[member], member); + const i = struct.memberNames.indexOf(member); + const memberSchema = struct.memberList[i]; + return this.memberFrom(Array.isArray(memberSchema) ? memberSchema : [memberSchema, 0], member); } if (this.isDocumentSchema()) { - return NormalizedSchema.memberFrom([SCHEMA.DOCUMENT, 0], member); + return this.memberFrom([SCHEMA.DOCUMENT, 0], member); } - throw new Error(`@smithy/core/schema - the schema ${this.getName(true)} does not have members.`); + throw new Error(`@smithy/core/schema - ${this.getName(true)} has no members.`); } /** @@ -415,22 +391,18 @@ export class NormalizedSchema implements INormalizedSchema { * * This does NOT return list and map members, it is only for structures. * + * @deprecated use (checked) structIterator instead. + * * @returns a map of member names to member schemas (normalized). */ public getMemberSchemas(): Record { - const { schema } = this; - const struct = schema as StructureSchema; - if (!struct || typeof struct !== "object") { - return {}; - } - if ("members" in struct) { - const buffer = {} as Record; - for (const member of struct.memberNames) { - buffer[member] = this.getMemberSchema(member)!; + const buffer = {} as any; + try { + for (const [k, v] of this.structIterator()) { + buffer[k] = v; } - return buffer; - } - return {}; + } catch (ignored) {} + return buffer; } /** @@ -459,14 +431,27 @@ export class NormalizedSchema implements INormalizedSchema { return; } if (!this.isStructSchema()) { - throw new Error("@smithy/core/schema - cannot acquire structIterator on non-struct schema."); + throw new Error("@smithy/core/schema - cannot iterate non-struct schema."); } const struct = this.getSchema() as StructureSchema; for (let i = 0; i < struct.memberNames.length; ++i) { - yield [struct.memberNames[i], NormalizedSchema.memberFrom([struct.memberList[i], 0], struct.memberNames[i])]; + yield [struct.memberNames[i], this.memberFrom([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. */ diff --git a/packages/core/src/submodules/schema/schemas/OperationSchema.ts b/packages/core/src/submodules/schema/schemas/OperationSchema.ts index e698af7804a..0b281851c8d 100644 --- a/packages/core/src/submodules/schema/schemas/OperationSchema.ts +++ b/packages/core/src/submodules/schema/schemas/OperationSchema.ts @@ -1,6 +1,5 @@ import type { OperationSchema as IOperationSchema, SchemaRef, SchemaTraits } from "@smithy/types"; -import { TypeRegistry } from "../TypeRegistry"; import { Schema } from "./Schema"; /** @@ -10,28 +9,29 @@ import { Schema } from "./Schema"; * @alpha */ export class OperationSchema extends Schema implements IOperationSchema { - public constructor( - public name: string, - public traits: SchemaTraits, - public input: SchemaRef, - public output: SchemaRef - ) { - super(name, traits); - } + public static readonly symbol = Symbol.for("@smithy/ope"); + public name!: string; + public traits!: SchemaTraits; + public input!: SchemaRef; + public output!: SchemaRef; + protected readonly symbol = OperationSchema.symbol; } /** * Factory for OperationSchema. * @internal */ -export function op( +export const op = ( namespace: string, name: string, - traits: SchemaTraits = {}, + traits: SchemaTraits, input: SchemaRef, output: SchemaRef -): OperationSchema { - const schema = new OperationSchema(namespace + "#" + name, traits, input, output); - TypeRegistry.for(namespace).register(name, schema); - return schema; -} +): OperationSchema => + Schema.assign(new OperationSchema(), { + name, + namespace, + traits, + input, + output, + }); diff --git a/packages/core/src/submodules/schema/schemas/Schema.ts b/packages/core/src/submodules/schema/schemas/Schema.ts index 916e5644599..cf71348be10 100644 --- a/packages/core/src/submodules/schema/schemas/Schema.ts +++ b/packages/core/src/submodules/schema/schemas/Schema.ts @@ -1,13 +1,34 @@ import type { SchemaTraits, TraitsSchema } from "@smithy/types"; +import { TypeRegistry } from "../TypeRegistry"; + /** * Abstract base for class-based Schema except NormalizedSchema. * * @alpha */ export abstract class Schema implements TraitsSchema { - protected constructor( - public name: string, - public traits: SchemaTraits - ) {} + public name!: string; + public namespace!: string; + public traits!: SchemaTraits; + protected abstract readonly symbol: symbol; + + public static assign(instance: T, values: Omit): T { + const schema = Object.assign(instance, values); + TypeRegistry.for(schema.namespace).register(schema.name, schema); + return schema; + } + + public static [Symbol.hasInstance](lhs: unknown) { + const isPrototype = this.prototype.isPrototypeOf(lhs as any); + if (!isPrototype && typeof lhs === "object" && lhs !== null) { + const list = lhs as any; + return list.symbol === (this as any).symbol; + } + return isPrototype; + } + + public getName(): string { + return this.namespace + "#" + this.name; + } } diff --git a/packages/core/src/submodules/schema/schemas/SimpleSchema.ts b/packages/core/src/submodules/schema/schemas/SimpleSchema.ts index 5cd52ebbc18..113a2c08434 100644 --- a/packages/core/src/submodules/schema/schemas/SimpleSchema.ts +++ b/packages/core/src/submodules/schema/schemas/SimpleSchema.ts @@ -10,25 +10,11 @@ import { Schema } from "./Schema"; * @alpha */ export class SimpleSchema extends Schema implements TraitsSchema { - public static symbol = Symbol.for("@smithy/core/schema::SimpleSchema"); - protected symbol = SimpleSchema.symbol; - - public constructor( - public name: string, - public schemaRef: SchemaRef, - public traits: SchemaTraits - ) { - super(name, traits); - } - - public static [Symbol.hasInstance](lhs: unknown): lhs is SimpleSchema { - const isPrototype = SimpleSchema.prototype.isPrototypeOf(lhs as any); - if (!isPrototype && typeof lhs === "object" && lhs !== null) { - const sim = lhs as SimpleSchema; - return sim.symbol === SimpleSchema.symbol; - } - return isPrototype; - } + public static readonly symbol = Symbol.for("@smithy/sim"); + public name!: string; + public schemaRef!: SchemaRef; + public traits!: SchemaTraits; + protected readonly symbol = SimpleSchema.symbol; } /** @@ -36,8 +22,10 @@ export class SimpleSchema extends Schema implements TraitsSchema { * * @internal */ -export function sim(namespace: string, name: string, schemaRef: SchemaRef, traits: SchemaTraits) { - const schema = new SimpleSchema(namespace + "#" + name, schemaRef, traits); - TypeRegistry.for(namespace).register(name, schema); - return schema; -} +export const sim = (namespace: string, name: string, schemaRef: SchemaRef, traits: SchemaTraits) => + Schema.assign(new SimpleSchema(), { + name, + namespace, + traits, + schemaRef, + }); diff --git a/packages/core/src/submodules/schema/schemas/StructureSchema.ts b/packages/core/src/submodules/schema/schemas/StructureSchema.ts index 1369c6a3a21..dc7d5093c28 100644 --- a/packages/core/src/submodules/schema/schemas/StructureSchema.ts +++ b/packages/core/src/submodules/schema/schemas/StructureSchema.ts @@ -9,33 +9,12 @@ import { Schema } from "./Schema"; * @alpha */ export class StructureSchema extends Schema implements IStructureSchema { - public static symbol = Symbol.for("@smithy/core/schema::StructureSchema"); - protected symbol = StructureSchema.symbol; - - public members: Record = {}; - - public constructor( - public name: string, - public traits: SchemaTraits, - public memberNames: string[], - public memberList: SchemaRef[] - ) { - super(name, traits); - for (let i = 0; i < memberNames.length; ++i) { - this.members[memberNames[i]] = Array.isArray(memberList[i]) - ? (memberList[i] as MemberSchema) - : [memberList[i], 0]; - } - } - - public static [Symbol.hasInstance](lhs: unknown): lhs is StructureSchema { - const isPrototype = StructureSchema.prototype.isPrototypeOf(lhs as any); - if (!isPrototype && typeof lhs === "object" && lhs !== null) { - const struct = lhs as StructureSchema; - return struct.symbol === StructureSchema.symbol; - } - return isPrototype; - } + public static symbol = Symbol.for("@smithy/str"); + public name!: string; + public traits!: SchemaTraits; + public memberNames!: string[]; + public memberList!: SchemaRef[]; + protected readonly symbol = StructureSchema.symbol; } /** @@ -43,14 +22,17 @@ export class StructureSchema extends Schema implements IStructureSchema { * * @internal */ -export function struct( +export const struct = ( namespace: string, name: string, traits: SchemaTraits, memberNames: string[], memberList: SchemaRef[] -): StructureSchema { - const schema = new StructureSchema(namespace + "#" + name, traits, memberNames, memberList); - TypeRegistry.for(namespace).register(name, schema); - return schema; -} +): StructureSchema => + Schema.assign(new StructureSchema(), { + name, + namespace, + traits, + memberNames, + memberList, + }); diff --git a/packages/core/src/submodules/schema/schemas/schemas.spec.ts b/packages/core/src/submodules/schema/schemas/schemas.spec.ts index a39e17a96e6..f7cc1b293c5 100644 --- a/packages/core/src/submodules/schema/schemas/schemas.spec.ts +++ b/packages/core/src/submodules/schema/schemas/schemas.spec.ts @@ -34,10 +34,11 @@ describe("schemas", () => { }); describe(ErrorSchema.name, () => { - const schema = new ErrorSchema("ack#Error", 0, [], [], Error); + const schema = error("ack", "Error", 0, [], [], Error); it("is a StructureSchema", () => { expect(schema).toBeInstanceOf(StructureSchema); + expect(schema).toBeInstanceOf(ErrorSchema); }); it("additionally defines an error constructor", () => { @@ -56,10 +57,12 @@ describe("schemas", () => { expect(object).toBeInstanceOf(ErrorSchema); }); }); + describe(ListSchema.name, () => { - const schema = new ListSchema("ack#List", 0, 0); + const schema = list("ack", "List", 0, 0); it("is a Schema", () => { expect(schema).toBeInstanceOf(Schema); + expect(schema).toBeInstanceOf(ListSchema); }); it("has a value schema", () => { expect(schema.valueSchema).toBe(0 as SchemaRef); @@ -74,10 +77,12 @@ describe("schemas", () => { expect(object).toBeInstanceOf(ListSchema); }); }); + describe(MapSchema.name, () => { - const schema = new MapSchema("ack#Map", 0, 0, 1); + const schema = map("ack", "Map", 0, 0, 1); it("is a Schema", () => { expect(schema).toBeInstanceOf(Schema); + expect(schema).toBeInstanceOf(MapSchema); }); it("has a key and value schema", () => { expect(schema.keySchema).toBe(0 as SchemaRef); @@ -93,10 +98,12 @@ describe("schemas", () => { expect(object).toBeInstanceOf(MapSchema); }); }); + describe(OperationSchema.name, () => { - const schema = new OperationSchema("ack#Operation", 0, "unit", "unit"); + const schema = op("ack", "Operation", 0, "unit", "unit"); it("is a Schema", () => { expect(schema).toBeInstanceOf(Schema); + expect(schema).toBeInstanceOf(OperationSchema); }); it("has an input and output schema", () => { expect(schema.input).toEqual("unit"); @@ -107,10 +114,14 @@ describe("schemas", () => { expect(TypeRegistry.for("ack").getSchema(schema.name)).toEqual(schema); }); }); + describe(Schema.name, () => { const schema = new (class extends Schema { + protected symbol = Symbol(); public constructor(name: string, traits: SchemaTraits) { - super(name, traits); + super(); + this.name = name; + this.traits = traits; } })("ack#Abstract", { a: 0, @@ -126,10 +137,12 @@ describe("schemas", () => { }); }); }); + describe(SimpleSchema.name, () => { - const schema = new SimpleSchema("ack#Simple", 0, 0); + const schema = sim("ack", "Simple", 0, 0); it("is a Schema", () => { expect(schema).toBeInstanceOf(Schema); + expect(schema).toBeInstanceOf(SimpleSchema); }); it("has a factory and the factory registers the schema", () => { expect(sim("ack", "Simple", 0, 0)).toEqual(schema); @@ -141,17 +154,17 @@ describe("schemas", () => { expect(object).toBeInstanceOf(SimpleSchema); }); }); + describe(StructureSchema.name, () => { - const schema = new StructureSchema("ack#Structure", 0, ["a", "b", "c"], [0, 1, 2]); + const schema = struct("ack", "Structure", 0, ["a", "b", "c"], [0, 1, 2]); it("is a Schema", () => { expect(schema).toBeInstanceOf(Schema); + expect(schema).toBeInstanceOf(StructureSchema); + expect(schema).not.toBeInstanceOf(ErrorSchema); }); it("has member schemas", () => { - expect(schema.members).toEqual({ - a: [0 as SchemaRef, 0 as SchemaTraits], - b: [1 as SchemaRef, 0 as SchemaTraits], - c: [2 as SchemaRef, 0 as SchemaTraits], - }); + expect(schema.memberNames).toEqual(["a", "b", "c"]); + expect(schema.memberList).toEqual([0, 1, 2]); }); it("has a factory and the factory registers the schema", () => { expect(struct("ack", "Structure", 0, ["a", "b", "c"], [0, 1, 2])).toEqual(schema); diff --git a/packages/types/src/schema/schema.ts b/packages/types/src/schema/schema.ts index 5d0652c3443..108ffefb375 100644 --- a/packages/types/src/schema/schema.ts +++ b/packages/types/src/schema/schema.ts @@ -50,6 +50,7 @@ export type SchemaTraits = TraitBitVector | SchemaTraitsObject; * @public */ export interface TraitsSchema { + namespace: string; name: string; traits: SchemaTraits; } @@ -155,7 +156,14 @@ export type SchemaTraitsObject = { export interface StructureSchema extends TraitsSchema { name: string; traits: SchemaTraits; - members: Record; + memberNames: string[]; + memberList: SchemaRef[]; + + /** + * @deprecated structure member iteration will be linear on the memberNames and memberList arrays. + * It can be collected into a hashmap form on an ad-hoc basis, but will not initialize as such. + */ + members?: Record | undefined; } /** @@ -226,7 +234,7 @@ export interface NormalizedSchema { * For struct/union. */ getMemberSchema(member: string): NormalizedSchema | undefined; - getMemberSchemas(): Record; + structIterator(): Generator<[string, NormalizedSchema], undefined, undefined>; } /** diff --git a/testbed/bundlers/source.ts b/testbed/bundlers/source.ts index 1720b066bce..9929198a817 100644 --- a/testbed/bundlers/source.ts +++ b/testbed/bundlers/source.ts @@ -1 +1 @@ -export { SmithyRpcV2CborProtocol } from "@smithy/core/cbor"; +export { NormalizedSchema, op, struct, error, list, map, sim } from "@smithy/core/schema";