diff --git a/packages/reflect/src/core/info.js b/packages/reflect/src/core/info.js index dad1b705..e674bfdc 100644 --- a/packages/reflect/src/core/info.js +++ b/packages/reflect/src/core/info.js @@ -1,16 +1,56 @@ /** @import { TypeId } from '@wimaengine/type' */ -export class TypeInfo {} + +export class TypeInfo { + + /** + * @returns {TypeInfoSerial} + */ + serialize() { + return /** @type {TypeInfoSerial} */ (/** @type {unknown} */ (TypeInfo.serialize(this))) + } + + /** + * @param {TypeInfo} _value + * @throws {TypeError} Always throws because `TypeInfo` is abstract. + */ + static serialize(_value) { + throw new TypeError('TypeInfo is abstract and cannot be serialized directly') + } + + /** + * @param {TypeInfoSerial} _value + * @param {TypeInfo} [_out] + * @throws {TypeError} Always throws because `TypeInfo` is abstract. + */ + static deserialize(_value, _out) { + throw new TypeError('TypeInfo is abstract and cannot be deserialized directly') + } + + /** + * @param {unknown} _value + * @throws {TypeError} Always throws because `TypeInfo` is abstract. + */ + static validateSerial(_value) { + throw new TypeError('TypeInfo is abstract and cannot validate serials directly') + } + + /** + * @param {unknown} _value + * @throws {TypeError} Always throws because `TypeInfo` is abstract. + */ + static validSerial(_value) { + throw new TypeError('TypeInfo is abstract and cannot validate serials directly') + } +} export class Field { /** - * @readonly * @type {TypeId} */ type /** - * @readonly * @type {boolean} */ optional @@ -23,25 +63,105 @@ export class Field { this.type = type this.optional = optional } + + /** + * @returns {FieldSerial} + */ + serialize() { + return Field.serialize(this) + } + + /** + * @param {Field} value + * @returns {FieldSerial} + */ + static serialize(value) { + return { + type: value.type, + optional: value.optional + } + } + + /** + * @param {FieldSerial} value + * @param {Field} [out] + * @returns {Field} + */ + static deserialize(value, out = new Field(value.type, value.optional)) { + out.type = value.type + out.optional = value.optional + + return out + } + + /** + * @param {unknown} value + * @returns {value is FieldSerial} + */ + static validateSerial(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value) || !('type' in value) || !('optional' in value)) { + return false + } + + return typeof value.type === 'string' && typeof value.optional === 'boolean' + } + } export class OpaqueInfo extends TypeInfo { + + /** + * @returns {OpaqueInfoSerial} + */ + serialize() { + return OpaqueInfo.serialize(this) + } + + /** + * @param {OpaqueInfo} _value + * @returns {OpaqueInfoSerial} + */ + static serialize(_value) { + return { + kind: 'opaque' + } + } + static default() { return new OpaqueInfo() } + + /** + * @param {OpaqueInfoSerial} _value + * @param {OpaqueInfo} [out] + * @returns {OpaqueInfo} + */ + static deserialize(_value, out = new OpaqueInfo()) { + return out + } + + /** + * @param {unknown} value + * @returns {value is OpaqueInfoSerial} + */ + static validateSerial(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value) || !('kind' in value)) { + return false + } + + return value.kind === 'opaque' + } } export class StructInfo extends TypeInfo { /** - * @readonly * @private * @type {Map} */ names = new Map() /** - * @readonly * @private * @type {Field[]} */ @@ -53,16 +173,13 @@ export class StructInfo extends TypeInfo { constructor(record) { super() - for (const name in record) { + for (const [name, value] of Object.entries(record)) { const position = this.fields.length this.names.set(name, position) - this.fields.push(record[name]) + this.fields.push(value) } } - static default() { - return new StructInfo({}) - } /** * @param {string} name @@ -91,13 +208,88 @@ export class StructInfo extends TypeInfo { size() { return this.fields.length } + + /** + * @returns {StructInfoSerial} + */ + serialize() { + return StructInfo.serialize(this) + } + + static default() { + return new StructInfo({}) + } + + /** + * @param {StructInfo} value + * @returns {StructInfoSerial} + */ + static serialize(value) { + + /** @type {Record} */ + const fields = {} + + for (const name of value.fieldNames()) { + const field = value.get(name) + + if (field !== undefined) { + fields[name] = Field.serialize(field) + } + } + + return { + kind: 'struct', + fields + } + } + + /** + * @param {StructInfoSerial} value + * @param {StructInfo} [out] + * @returns {StructInfo} + */ + static deserialize(value, out = new StructInfo({})) { + out.names.clear() + out.fields.length = 0 + + for (const [name, fieldSerial] of Object.entries(value.fields)) { + out.names.set(name, out.fields.length) + out.fields.push(Field.deserialize(fieldSerial)) + } + + return out + } + + /** + * @param {unknown} value + * @returns {value is StructInfoSerial} + */ + static validateSerial(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value) || !('kind' in value) || !('fields' in value)) { + return false + } + + if (value.kind !== 'struct' || + typeof value.fields !== 'object' || + value.fields === null || + Array.isArray(value.fields)) { + return false + } + + for (const field of Object.values(value.fields)) { + if (!Field.validateSerial(field)) { + return false + } + } + + return true + } } export class EnumInfo extends TypeInfo { /** - * @readonly - * @type {ReadonlyMap} + * @type {Map} */ variants @@ -117,7 +309,7 @@ export class EnumInfo extends TypeInfo { /** * @param {string} variant - * @returns {number} + * @returns {number | undefined} */ get(variant) { return this.variants.get(variant) @@ -133,21 +325,77 @@ export class EnumInfo extends TypeInfo { /** * @returns {MapIterator<[string,number]>} */ - * [Symbol.iterator]() { + [Symbol.iterator]() { return this.variants.entries() } + + /** + * @returns {EnumInfoSerial} + */ + serialize() { + return EnumInfo.serialize(this) + } + + /** + * @param {EnumInfo} value + * @returns {EnumInfoSerial} + */ + static serialize(value) { + return { + kind: 'enum', + variants: Object.fromEntries(value.variants) + } + } + + /** + * @param {EnumInfoSerial} value + * @param {EnumInfo} [out] + * @returns {EnumInfo} + */ + static deserialize(value, out = new EnumInfo(value.variants)) { + out.variants.clear() + + for (const [variant, discriminant] of Object.entries(value.variants)) { + out.variants.set(variant, discriminant) + } + + return out + } + + /** + * @param {unknown} value + * @returns {value is EnumInfoSerial} + */ + static validateSerial(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value) || !('kind' in value) || !('variants' in value)) { + return false + } + + if (value.kind !== 'enum' || + typeof value.variants !== 'object' || + value.variants === null || + Array.isArray(value.variants)) { + return false + } + + for (const discriminant of Object.values(value.variants)) { + if (typeof discriminant !== 'number') { + return false + } + } + + return true + } } export class FunctionInfo extends TypeInfo { /** - * @readonly - * @type {ReadonlyArray} + * @type {TypeId[]} */ parameterTypes /** - * @readonly * @type {TypeId} */ returnType @@ -183,12 +431,67 @@ export class FunctionInfo extends TypeInfo { getReturnType() { return this.returnType } + + /** + * @returns {FunctionInfoSerial} + */ + serialize() { + return FunctionInfo.serialize(this) + } + + /** + * @param {FunctionInfo} value + * @returns {FunctionInfoSerial} + */ + static serialize(value) { + return { + kind: 'function', + parameterTypes: [...value.parameterTypes], + returnType: value.returnType + } + } + + /** + * @param {FunctionInfoSerial} value + * @param {FunctionInfo} [out] + * @returns {FunctionInfo} + */ + static deserialize(value, out = new FunctionInfo([...value.parameterTypes], value.returnType)) { + out.parameterTypes.length = 0 + out.parameterTypes.push(...value.parameterTypes) + out.returnType = value.returnType + + return out + } + + /** + * @param {unknown} value + * @returns {value is FunctionInfoSerial} + */ + static validateSerial(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value) || !('kind' in value) || !('parameterTypes' in value) || !('returnType' in value)) { + return false + } + + if (value.kind !== 'function') { + return false + } + + if (!Array.isArray(value.parameterTypes)) { + return false + } + + if (typeof value.returnType !== 'string') { + return false + } + + return value.parameterTypes.every((typeId) => typeof typeId === 'string') + } } export class ArrayInfo extends TypeInfo { /** - * @readonly * @type {TypeId} */ elementType @@ -207,12 +510,52 @@ export class ArrayInfo extends TypeInfo { getElementType() { return this.elementType } + + /** + * @returns {ArrayInfoSerial} + */ + serialize() { + return ArrayInfo.serialize(this) + } + + /** + * @param {ArrayInfo} value + * @returns {ArrayInfoSerial} + */ + static serialize(value) { + return { + kind: 'array', + elementType: value.elementType + } + } + + /** + * @param {ArrayInfoSerial} value + * @param {ArrayInfo} [out] + * @returns {ArrayInfo} + */ + static deserialize(value, out = new ArrayInfo(value.elementType)) { + out.elementType = value.elementType + + return out + } + + /** + * @param {unknown} value + * @returns {value is ArrayInfoSerial} + */ + static validateSerial(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value) || !('kind' in value) || !('elementType' in value)) { + return false + } + + return value.kind === 'array' && typeof value.elementType === 'string' + } } export class SetInfo extends TypeInfo { /** - * @readonly * @type {TypeId} */ elementType @@ -231,18 +574,57 @@ export class SetInfo extends TypeInfo { getElementType() { return this.elementType } + + /** + * @returns {SetInfoSerial} + */ + serialize() { + return SetInfo.serialize(this) + } + + /** + * @param {SetInfo} value + * @returns {SetInfoSerial} + */ + static serialize(value) { + return { + kind: 'set', + elementType: value.elementType + } + } + + /** + * @param {SetInfoSerial} value + * @param {SetInfo} [out] + * @returns {SetInfo} + */ + static deserialize(value, out = new SetInfo(value.elementType)) { + out.elementType = value.elementType + + return out + } + + /** + * @param {unknown} value + * @returns {value is SetInfoSerial} + */ + static validateSerial(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value) || !('kind' in value) || !('elementType' in value)) { + return false + } + + return value.kind === 'set' && typeof value.elementType === 'string' + } } export class MapInfo extends TypeInfo { /** - * @readonly * @type {TypeId} */ keyType /** - * @readonly * @type {TypeId} */ valueType @@ -270,13 +652,57 @@ export class MapInfo extends TypeInfo { getValueType() { return this.valueType } + + /** + * @returns {MapInfoSerial} + */ + serialize() { + return MapInfo.serialize(this) + } + + /** + * @param {MapInfo} value + * @returns {MapInfoSerial} + */ + static serialize(value) { + return { + kind: 'map', + keyType: value.keyType, + valueType: value.valueType + } + } + + /** + * @param {MapInfoSerial} value + * @param {MapInfo} [out] + * @returns {MapInfo} + */ + static deserialize(value, out = new MapInfo(value.keyType, value.valueType)) { + out.keyType = value.keyType + out.valueType = value.valueType + + return out + } + + /** + * @param {unknown} value + * @returns {value is MapInfoSerial} + */ + static validateSerial(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value) || !('kind' in value) || !('keyType' in value) || !('valueType' in value)) { + return false + } + + return value.kind === 'map' && + typeof value.keyType === 'string' && + typeof value.valueType === 'string' + } } export class TupleInfo extends TypeInfo { /** - * @readonly - * @type {ReadonlyArray} + * @type {TypeId[]} */ elementTypes @@ -302,4 +728,110 @@ export class TupleInfo extends TypeInfo { getElements() { return this.elementTypes } + + /** + * @returns {TupleInfoSerial} + */ + serialize() { + return TupleInfo.serialize(this) + } + + /** + * @param {TupleInfo} value + * @returns {TupleInfoSerial} + */ + static serialize(value) { + return { + kind: 'tuple', + elementTypes: [...value.elementTypes] + } + } + + /** + * @param {TupleInfoSerial} value + * @param {TupleInfo} [out] + * @returns {TupleInfo} + */ + static deserialize(value, out = new TupleInfo([...value.elementTypes])) { + out.elementTypes.length = 0 + out.elementTypes.push(...value.elementTypes) + + return out + } + + /** + * @param {unknown} value + * @returns {value is TupleInfoSerial} + */ + static validateSerial(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value) || !('kind' in value) || !('elementTypes' in value)) { + return false + } + + if (value.kind !== 'tuple') { + return false + } + + return Array.isArray(value.elementTypes) && + value.elementTypes.every((typeId) => typeof typeId === 'string') + } } + +/** + * @typedef OpaqueInfoSerial + * @property {'opaque'} kind + */ + +/** + * @typedef FieldSerial + * @property {TypeId} type + * @property {boolean} optional + */ + +/** + * @typedef StructInfoSerial + * @property {'struct'} kind + * @property {Record} fields + */ + +/** + * @typedef EnumInfoSerial + * @property {'enum'} kind + * @property {Record} variants + */ + +/** + * @typedef FunctionInfoSerial + * @property {'function'} kind + * @property {TypeId[]} parameterTypes + * @property {TypeId} returnType + */ + +/** + * @typedef ArrayInfoSerial + * @property {'array'} kind + * @property {TypeId} elementType + */ + +/** + * @typedef SetInfoSerial + * @property {'set'} kind + * @property {TypeId} elementType + */ + +/** + * @typedef MapInfoSerial + * @property {'map'} kind + * @property {TypeId} keyType + * @property {TypeId} valueType + */ + +/** + * @typedef TupleInfoSerial + * @property {'tuple'} kind + * @property {TypeId[]} elementTypes + */ + +/** + * @typedef {OpaqueInfoSerial | StructInfoSerial | EnumInfoSerial | FunctionInfoSerial | ArrayInfoSerial | SetInfoSerial | MapInfoSerial | TupleInfoSerial} TypeInfoSerial + */ diff --git a/packages/reflect/src/resources/typeregistry.js b/packages/reflect/src/resources/typeregistry.js index c0772ff0..0628b65a 100644 --- a/packages/reflect/src/resources/typeregistry.js +++ b/packages/reflect/src/resources/typeregistry.js @@ -1,6 +1,17 @@ /** @import { TypeId, Constructor } from '@wimaengine/type' */ -import { typeid } from '@wimaengine/type' -import { TypeInfo } from '../core' +/** @import { TypeInfoSerial } from '../core' */ +import { setTypeId, typeid } from '@wimaengine/type' +import { + ArrayInfo, + EnumInfo, + FunctionInfo, + MapInfo, + OpaqueInfo, + SetInfo, + StructInfo, + TupleInfo, + TypeInfo +} from '../core' export class TypeRegistry { @@ -64,12 +75,60 @@ export class TypeRegistry { getByTypeId(typeId) { return this.inner.get(typeId) } + + /** + * @param {TypeRegistry} value + * @returns {TypeRegistrySerial} + */ + static serialize(value) { + + /** @type {TypeRegistrySerial} */ + const serial = {} + + for (const [typeId, entry] of value.inner) { + serial[typeId] = entry.serialize() + } + + return serial + } + + /** + * @param {TypeRegistrySerial} value + * @param {TypeRegistry} [out] + * @returns {TypeRegistry} + */ + static deserialize(value, out = new TypeRegistry()) { + out.inner.clear() + + for (const [typeId, info] of Object.entries(value)) { + out.registerTypeId(setTypeId(typeId), TypeEntry.deserialize(info)) + } + + return out + } + + /** + * @param {unknown} value + * @returns {value is TypeRegistrySerial} + */ + static validateSerial(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return false + } + + for (const info of Object.values(value)) { + if (!TypeEntry.validateSerial(info)) { + return false + } + } + + return true + } } export class TypeEntry { /** - * @readonly * @type {TypeInfo} */ info @@ -94,6 +153,150 @@ export class TypeEntry { this.constructorFn = constructorFn } + /** + * @returns {TypeInfoSerial} + */ + serialize() { + return this.info.serialize() + } + + /** + * @param {TypeInfo} value + * @returns {TypeInfoSerial} + * @throws {TypeError} If `value` is not a supported `TypeInfo` subtype. + */ + static serialize(value) { + if (value instanceof OpaqueInfo) { + return OpaqueInfo.serialize(value) + } + + if (value instanceof StructInfo) { + return StructInfo.serialize(value) + } + + if (value instanceof EnumInfo) { + return EnumInfo.serialize(value) + } + + if (value instanceof FunctionInfo) { + return FunctionInfo.serialize(value) + } + + if (value instanceof ArrayInfo) { + return ArrayInfo.serialize(value) + } + + if (value instanceof SetInfo) { + return SetInfo.serialize(value) + } + + if (value instanceof MapInfo) { + return MapInfo.serialize(value) + } + + if (value instanceof TupleInfo) { + return TupleInfo.serialize(value) + } + + throw new TypeError(`Unsupported TypeInfo: ${value?.constructor?.name || 'unknown'}`) + } + + /** + * @param {TypeInfoSerial} value + * @param {TypeInfo} [out] + * @returns {TypeInfo} + * @throws {TypeError} If `value` is not a supported `TypeInfoSerial` kind. + */ + static deserialize(value, out) { + if (typeof value !== 'object' || value === null || Array.isArray(value) || !('kind' in value)) { + throw new TypeError('Unsupported TypeInfo serial kind') + } + + switch (value.kind) { + case 'opaque': + return out instanceof OpaqueInfo ? + OpaqueInfo.deserialize(value, out) : + OpaqueInfo.deserialize(value) + + case 'struct': + return out instanceof StructInfo ? + StructInfo.deserialize(value, out) : + StructInfo.deserialize(value) + + case 'enum': + return out instanceof EnumInfo ? + EnumInfo.deserialize(value, out) : + EnumInfo.deserialize(value) + + case 'function': + return out instanceof FunctionInfo ? + FunctionInfo.deserialize(value, out) : + FunctionInfo.deserialize(value) + + case 'array': + return out instanceof ArrayInfo ? + ArrayInfo.deserialize(value, out) : + ArrayInfo.deserialize(value) + + case 'set': + return out instanceof SetInfo ? + SetInfo.deserialize(value, out) : + SetInfo.deserialize(value) + + case 'map': + return out instanceof MapInfo ? + MapInfo.deserialize(value, out) : + MapInfo.deserialize(value) + + case 'tuple': + return out instanceof TupleInfo ? + TupleInfo.deserialize(value, out) : + TupleInfo.deserialize(value) + + default: + throw new TypeError('Unsupported TypeInfo serial kind') + } + } + + /** + * @param {unknown} value + * @returns {value is TypeInfoSerial} + */ + static validateSerial(value) { + if (typeof value !== 'object' || value === null || Array.isArray(value) || !('kind' in value)) { + return false + } + + switch (value.kind) { + case 'opaque': + return OpaqueInfo.validateSerial(value) + + case 'struct': + return StructInfo.validateSerial(value) + + case 'enum': + return EnumInfo.validateSerial(value) + + case 'function': + return FunctionInfo.validateSerial(value) + + case 'array': + return ArrayInfo.validateSerial(value) + + case 'set': + return SetInfo.validateSerial(value) + + case 'map': + return MapInfo.validateSerial(value) + + case 'tuple': + return TupleInfo.validateSerial(value) + + default: + return false + } + } + /** * @template {unknown[]} T * @param {string} name @@ -156,3 +359,7 @@ export class MethodEntry { return this.method(...(args || [])) } } + +/** + * @typedef {Record} TypeRegistrySerial + */ diff --git a/packages/reflect/tests/typeregistry.test.js b/packages/reflect/tests/typeregistry.test.js index c538b505..040387ca 100644 --- a/packages/reflect/tests/typeregistry.test.js +++ b/packages/reflect/tests/typeregistry.test.js @@ -1,8 +1,8 @@ import { test, describe } from "vitest"; -import { deepStrictEqual, strictEqual } from "node:assert"; -import { Field, OpaqueInfo, StructInfo, EnumInfo, FunctionInfo } from "../src/core"; +import { deepStrictEqual, strictEqual, throws } from "node:assert"; +import { Field, OpaqueInfo, StructInfo, EnumInfo, FunctionInfo, ArrayInfo, SetInfo, MapInfo, TupleInfo, TypeInfo } from "../src/core"; import { setTypeId, typeid, typeidFunction } from "@wimaengine/type"; -import { TypeRegistry } from "../src/resources"; +import { TypeEntry, TypeRegistry } from "../src/resources"; describe("Testing `TypeRegistry`", () => { const TestEnum = { @@ -96,4 +96,263 @@ describe("Testing `TypeRegistry`", () => { registry.register(Test, StructInfo.default()) deepStrictEqual(registry.getByTypeId(id)?.info, StructInfo.default()) }) + + test("serializes and deserializes type info schema", () => { + const registry = new TypeRegistry() + const typeIdByName = { + opaque: setTypeId(OpaqueInfo.name), + struct: setTypeId(StructInfo.name), + enum: setTypeId(EnumInfo.name), + function: setTypeId(FunctionInfo.name), + array: setTypeId(ArrayInfo.name), + set: setTypeId(SetInfo.name), + map: setTypeId(MapInfo.name), + tuple: setTypeId(TupleInfo.name) + } + + registry.registerTypeId(typeIdByName.opaque, OpaqueInfo.default()) + registry.registerTypeId(typeIdByName.struct, new StructInfo({ + age: new Field(typeid(Number)), + name: new Field(typeid(String), true) + })) + registry.registerTypeId(typeIdByName.enum, new EnumInfo({ + Variant1: 0, + Variant2: 1 + })) + registry.registerTypeId(typeIdByName.function, new FunctionInfo([typeid(Number), typeid(String)], typeid(Boolean))) + registry.registerTypeId(typeIdByName.array, new ArrayInfo(typeid(Number))) + registry.registerTypeId(typeIdByName.set, new SetInfo(typeid(String))) + registry.registerTypeId(typeIdByName.map, new MapInfo(typeid(String), typeid(Number))) + registry.registerTypeId(typeIdByName.tuple, new TupleInfo([typeid(Number), typeid(String)])) + + const registrySerial = TypeRegistry.serialize(registry) + const restoredRegistry = TypeRegistry.deserialize(registrySerial) + + strictEqual(TypeRegistry.validateSerial(registrySerial), true) + deepStrictEqual(registrySerial, { + [typeIdByName.opaque]: { + kind: 'opaque' + }, + [typeIdByName.struct]: { + kind: 'struct', + fields: { + age: { + type: typeid(Number), + optional: false + }, + name: { + type: typeid(String), + optional: true + } + } + }, + [typeIdByName.enum]: { + kind: 'enum', + variants: { + Variant1: 0, + Variant2: 1 + } + }, + [typeIdByName.function]: { + kind: 'function', + parameterTypes: [typeid(Number), typeid(String)], + returnType: typeid(Boolean) + }, + [typeIdByName.array]: { + kind: 'array', + elementType: typeid(Number) + }, + [typeIdByName.set]: { + kind: 'set', + elementType: typeid(String) + }, + [typeIdByName.map]: { + kind: 'map', + keyType: typeid(String), + valueType: typeid(Number) + }, + [typeIdByName.tuple]: { + kind: 'tuple', + elementTypes: [typeid(Number), typeid(String)] + } + }) + deepStrictEqual(restoredRegistry.getByTypeId(typeIdByName.opaque)?.info, OpaqueInfo.default()) + deepStrictEqual(restoredRegistry.getByTypeId(typeIdByName.struct)?.info, new StructInfo({ + age: new Field(typeid(Number)), + name: new Field(typeid(String), true) + })) + deepStrictEqual(restoredRegistry.getByTypeId(typeIdByName.enum)?.info, new EnumInfo({ + Variant1: 0, + Variant2: 1 + })) + deepStrictEqual(restoredRegistry.getByTypeId(typeIdByName.function)?.info, new FunctionInfo([typeid(Number), typeid(String)], typeid(Boolean))) + deepStrictEqual(restoredRegistry.getByTypeId(typeIdByName.array)?.info, new ArrayInfo(typeid(Number))) + deepStrictEqual(restoredRegistry.getByTypeId(typeIdByName.set)?.info, new SetInfo(typeid(String))) + deepStrictEqual(restoredRegistry.getByTypeId(typeIdByName.map)?.info, new MapInfo(typeid(String), typeid(Number))) + deepStrictEqual(restoredRegistry.getByTypeId(typeIdByName.tuple)?.info, new TupleInfo([typeid(Number), typeid(String)])) + }) +}) + +describe("Testing `TypeInfo` serialization", () => { + test("round-trips OpaqueInfo", () => { + const info = OpaqueInfo.default() + const serial = { + kind: 'opaque' + } + + deepStrictEqual(TypeEntry.serialize(info), serial) + deepStrictEqual(info.serialize(), serial) + strictEqual(TypeEntry.validateSerial(serial), true) + strictEqual(OpaqueInfo.validateSerial(serial), true) + deepStrictEqual(TypeEntry.deserialize(serial), info) + deepStrictEqual(OpaqueInfo.deserialize(serial), info) + }) + + test("round-trips StructInfo", () => { + const info = new StructInfo({ + age: new Field(typeid(Number)), + name: new Field(typeid(String), true) + }) + const serial = { + kind: 'struct', + fields: { + age: { + type: typeid(Number), + optional: false + }, + name: { + type: typeid(String), + optional: true + } + } + } + + deepStrictEqual(TypeEntry.serialize(info), serial) + deepStrictEqual(info.serialize(), serial) + strictEqual(TypeEntry.validateSerial(serial), true) + strictEqual(StructInfo.validateSerial(serial), true) + deepStrictEqual(TypeEntry.deserialize(serial), info) + deepStrictEqual(StructInfo.deserialize(serial), info) + }) + + test("round-trips EnumInfo", () => { + const info = new EnumInfo({ + Variant1: 0, + Variant2: 1 + }) + const serial = { + kind: 'enum', + variants: { + Variant1: 0, + Variant2: 1 + } + } + + deepStrictEqual(TypeEntry.serialize(info), serial) + deepStrictEqual(info.serialize(), serial) + strictEqual(TypeEntry.validateSerial(serial), true) + strictEqual(EnumInfo.validateSerial(serial), true) + deepStrictEqual(TypeEntry.deserialize(serial), info) + deepStrictEqual(EnumInfo.deserialize(serial), info) + }) + + test("round-trips FunctionInfo", () => { + const info = new FunctionInfo([typeid(Number), typeid(String)], typeid(Boolean)) + const serial = { + kind: 'function', + parameterTypes: [typeid(Number), typeid(String)], + returnType: typeid(Boolean) + } + + deepStrictEqual(TypeEntry.serialize(info), serial) + deepStrictEqual(info.serialize(), serial) + strictEqual(TypeEntry.validateSerial(serial), true) + strictEqual(FunctionInfo.validateSerial(serial), true) + deepStrictEqual(TypeEntry.deserialize(serial), info) + deepStrictEqual(FunctionInfo.deserialize(serial), info) + }) + + test("round-trips ArrayInfo", () => { + const info = new ArrayInfo(typeid(Number)) + const serial = { + kind: 'array', + elementType: typeid(Number) + } + + deepStrictEqual(TypeEntry.serialize(info), serial) + deepStrictEqual(info.serialize(), serial) + strictEqual(TypeEntry.validateSerial(serial), true) + strictEqual(ArrayInfo.validateSerial(serial), true) + deepStrictEqual(TypeEntry.deserialize(serial), info) + deepStrictEqual(ArrayInfo.deserialize(serial), info) + }) + + test("round-trips SetInfo", () => { + const info = new SetInfo(typeid(String)) + const serial = { + kind: 'set', + elementType: typeid(String) + } + + deepStrictEqual(TypeEntry.serialize(info), serial) + deepStrictEqual(info.serialize(), serial) + strictEqual(TypeEntry.validateSerial(serial), true) + strictEqual(SetInfo.validateSerial(serial), true) + deepStrictEqual(TypeEntry.deserialize(serial), info) + deepStrictEqual(SetInfo.deserialize(serial), info) + }) + + test("round-trips MapInfo", () => { + const info = new MapInfo(typeid(String), typeid(Number)) + const serial = { + kind: 'map', + keyType: typeid(String), + valueType: typeid(Number) + } + + deepStrictEqual(TypeEntry.serialize(info), serial) + deepStrictEqual(info.serialize(), serial) + strictEqual(TypeEntry.validateSerial(serial), true) + strictEqual(MapInfo.validateSerial(serial), true) + deepStrictEqual(TypeEntry.deserialize(serial), info) + deepStrictEqual(MapInfo.deserialize(serial), info) + }) + + test("round-trips TupleInfo", () => { + const info = new TupleInfo([typeid(Number), typeid(String)]) + const serial = { + kind: 'tuple', + elementTypes: [typeid(Number), typeid(String)] + } + + deepStrictEqual(TypeEntry.serialize(info), serial) + deepStrictEqual(info.serialize(), serial) + strictEqual(TypeEntry.validateSerial(serial), true) + strictEqual(TupleInfo.validateSerial(serial), true) + deepStrictEqual(TypeEntry.deserialize(serial), info) + deepStrictEqual(TupleInfo.deserialize(serial), info) + }) + + test("`TypeInfo` base methods throw", () => { + const info = OpaqueInfo.default() + const serial = { + kind: 'opaque' + } + + throws(() => TypeInfo.serialize(info)) + throws(() => TypeInfo.deserialize(serial)) + throws(() => TypeInfo.validateSerial(serial)) + }) + + test("rejects invalid type info serials", () => { + strictEqual(TypeEntry.validateSerial({ + kind: 'struct', + fields: { + age: { + type: 42, + optional: false + } + } + }), false) + }) })