From 9dada3c44e8c869446be56169ccdbb47f4ff6246 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Fri, 13 Mar 2026 17:46:11 +0100 Subject: [PATCH] feat: Better recursion --- apps/examples/genericEnumTypes/index.ts | 8 +- packages/typed-binary/src/index.ts | 41 +-- packages/typed-binary/src/main-api.ts | 38 ++- packages/typed-binary/src/structure/array.ts | 46 ++- .../typed-binary/src/structure/baseTypes.ts | 71 +++-- packages/typed-binary/src/structure/chars.ts | 18 +- packages/typed-binary/src/structure/concat.ts | 2 +- .../src/structure/dynamicArray.ts | 81 ++--- .../src/structure/genericObject.ts | 159 ++++++++++ packages/typed-binary/src/structure/keyed.ts | 147 --------- packages/typed-binary/src/structure/object.ts | 300 ------------------ .../typed-binary/src/structure/optional.ts | 44 ++- packages/typed-binary/src/structure/struct.ts | 111 +++++++ packages/typed-binary/src/structure/tuple.ts | 70 ++-- .../typed-binary/src/structure/typedArray.ts | 51 ++- packages/typed-binary/src/structure/types.ts | 153 ++------- packages/typed-binary/src/test/bool.test.ts | 9 +- packages/typed-binary/src/test/chars.test.ts | 6 +- .../src/test/dynamicArray.test.ts | 18 +- .../test/{parsed.test.ts => extract.test.ts} | 113 ++++--- packages/typed-binary/src/test/float.test.ts | 68 ++-- .../typed-binary/src/test/helpers/mock.ts | 12 +- packages/typed-binary/src/test/int.test.ts | 28 +- packages/typed-binary/src/test/keyed.test.ts | 281 ---------------- .../typed-binary/src/test/optional.test.ts | 4 +- .../test/{object.test.ts => struct.test.ts} | 106 ++++--- packages/typed-binary/src/test/tuple.test.ts | 12 +- .../typed-binary/src/test/typedArray.test.ts | 4 +- packages/typed-binary/src/test/unwrap.test.ts | 130 -------- packages/typed-binary/src/utilityTypes.ts | 32 +- 30 files changed, 712 insertions(+), 1451 deletions(-) create mode 100644 packages/typed-binary/src/structure/genericObject.ts delete mode 100644 packages/typed-binary/src/structure/keyed.ts delete mode 100644 packages/typed-binary/src/structure/object.ts create mode 100644 packages/typed-binary/src/structure/struct.ts rename packages/typed-binary/src/test/{parsed.test.ts => extract.test.ts} (51%) delete mode 100644 packages/typed-binary/src/test/keyed.test.ts rename packages/typed-binary/src/test/{object.test.ts => struct.test.ts} (66%) delete mode 100644 packages/typed-binary/src/test/unwrap.test.ts diff --git a/apps/examples/genericEnumTypes/index.ts b/apps/examples/genericEnumTypes/index.ts index c4f1669..be5f8ed 100644 --- a/apps/examples/genericEnumTypes/index.ts +++ b/apps/examples/genericEnumTypes/index.ts @@ -16,14 +16,14 @@ const Animal = bin.genericEnum( age: bin.i32, }, { - [AnimalType.DOG]: bin.object({ + [AnimalType.DOG]: { // Animal can be a dog breed: bin.string, - }), - [AnimalType.CAT]: bin.object({ + }, + [AnimalType.CAT]: { // Animal can be a cat striped: bin.bool, - }), + }, }, ); diff --git a/packages/typed-binary/src/index.ts b/packages/typed-binary/src/index.ts index 6f28f2c..8248d13 100644 --- a/packages/typed-binary/src/index.ts +++ b/packages/typed-binary/src/index.ts @@ -1,45 +1,6 @@ import * as bin from './main-api.ts'; -export * from './main-api.ts'; export { bin }; export default bin; export { getSystemEndianness } from './util.ts'; -export { MaxValue, SubTypeKey, Schema } from './structure/types.ts'; -export { - BoolSchema, - Float16Schema, - Float32Schema, - Int16Schema, - Int32Schema, - Int8Schema, - StringSchema, - Uint16Schema, - Uint32Schema, - Uint8Schema, - /** @deprecated Use Uint8Schema instead. */ - Uint8Schema as ByteSchema, -} from './structure/baseTypes.ts'; -export { ArraySchema } from './structure/array.ts'; -export { CharsSchema } from './structure/chars.ts'; -export { DynamicArraySchema } from './structure/dynamicArray.ts'; -export { KeyedSchema } from './structure/keyed.ts'; -export { ObjectSchema, GenericObjectSchema } from './structure/object.ts'; -export { OptionalSchema } from './structure/optional.ts'; -export { TupleSchema } from './structure/tuple.ts'; -export { TypedArraySchema } from './structure/typedArray.ts'; - -export type { AnyObjectSchema } from './structure/object.ts'; -export type { - Unwrap, - UnwrapRecord, - UnwrapArray, - IKeyedSchema, - Ref, - IRefResolver, - ISchema, - AnyKeyedSchema, - AnySchema, - AnySchemaWithProperties, - ISchemaWithProperties, -} from './structure/types.ts'; -export type { ParseUnwrapped } from './utilityTypes.ts'; +export { SubTypeKey } from './structure/types.ts'; diff --git a/packages/typed-binary/src/main-api.ts b/packages/typed-binary/src/main-api.ts index dd03cf1..b2d39b1 100644 --- a/packages/typed-binary/src/main-api.ts +++ b/packages/typed-binary/src/main-api.ts @@ -1,12 +1,33 @@ -export { arrayOf } from './structure/array.ts'; -export { bool, byte, i8, u8, i16, u16, i32, u32, f16, f32, string } from './structure/baseTypes.ts'; -export { chars } from './structure/chars.ts'; +export { array, type Array } from './structure/array.ts'; +export { + bool, + i8, + u8, + i16, + u16, + i32, + u32, + f16, + f32, + string, + type Bool, + type Int8, + type Uint8, + type Int16, + type Uint16, + type Int32, + type Uint32, + type Float16, + type Float32, + type String, +} from './structure/baseTypes.ts'; +export { chars, type Chars } from './structure/chars.ts'; export { concat } from './structure/concat.ts'; -export { dynamicArrayOf } from './structure/dynamicArray.ts'; -export { keyed } from './structure/keyed.ts'; -export { object, generic, genericEnum } from './structure/object.ts'; +export { dynamicArray, type DynamicArray } from './structure/dynamicArray.ts'; +export { struct, type Struct } from './structure/struct.ts'; +export { generic, genericEnum, type GenericObjectSchema } from './structure/genericObject.ts'; export { optional } from './structure/optional.ts'; -export { tupleOf } from './structure/tuple.ts'; +export { tuple, type Tuple } from './structure/tuple.ts'; export { f32Array, f64Array, @@ -17,6 +38,7 @@ export { u32Array, u8Array, u8ClampedArray, + type TypedArraySchema, } from './structure/typedArray.ts'; export { MaxValue } from './structure/types.ts'; @@ -26,4 +48,4 @@ export { Measurer } from './io/measurer.ts'; export { UnresolvedReferenceError, ValidationError } from './error.ts'; export type { Endianness, IMeasurer, ISerialInput, ISerialOutput } from './io/types.ts'; -export type { Parsed } from './utilityTypes.ts'; +export type { ExtractIn, ExtractOut, Schema } from './structure/types.ts'; diff --git a/packages/typed-binary/src/structure/array.ts b/packages/typed-binary/src/structure/array.ts index df18cbb..6022fad 100644 --- a/packages/typed-binary/src/structure/array.ts +++ b/packages/typed-binary/src/structure/array.ts @@ -1,28 +1,26 @@ import { ValidationError } from '../error.ts'; import { Measurer } from '../io/measurer.ts'; import type { IMeasurer, ISerialInput, ISerialOutput } from '../io/types.ts'; -import type { ParseUnwrapped } from '../utilityTypes.ts'; -import { type AnySchema, type IRefResolver, MaxValue, Schema, type Unwrap } from './types.ts'; +import { type ExtractIn, type ExtractOut, MaxValue, type Schema } from './types.ts'; -export class ArraySchema extends Schema[]> { - private elementSchema: TElement; - - constructor( - private readonly _unstableElementSchema: TElement, - public readonly length: number, - ) { - super(); +export interface Array extends Schema< + readonly ExtractIn[], + ExtractOut[] +> { + readonly elementSchema: TElement; + readonly length: number; +} - // In case this array isn't part of a keyed chain, - // let's assume the inner type is stable. - this.elementSchema = _unstableElementSchema; - } +class ArraySchema implements Array { + readonly elementSchema: TElement; + readonly length: number; - override resolveReferences(ctx: IRefResolver): void { - this.elementSchema = ctx.resolve(this._unstableElementSchema); + constructor(elementSchema: TElement, length: number) { + this.elementSchema = elementSchema; + this.length = length; } - override write(output: ISerialOutput, values: ParseUnwrapped[]): void { + write(output: ISerialOutput, values: readonly ExtractIn[]): void { if (values.length !== this.length) { throw new ValidationError(`Expected array of length ${this.length}, got ${values.length}`); } @@ -32,11 +30,11 @@ export class ArraySchema extends Schema[] { - const array: ParseUnwrapped[] = []; + read(input: ISerialInput): ExtractOut[] { + const array: ExtractOut[] = []; for (let i = 0; i < this.length; ++i) { - array.push(this.elementSchema.read(input) as ParseUnwrapped); + array.push(this.elementSchema.read(input)); } return array; @@ -54,8 +52,8 @@ export class ArraySchema extends Schema[] | MaxValue, + measure( + values: readonly ExtractIn[] | MaxValue, measurer: IMeasurer = new Measurer(), ): IMeasurer { for (let i = 0; i < this.length; ++i) { @@ -66,8 +64,8 @@ export class ArraySchema extends Schema( +/*#__NO_SIDE_EFFECTS__*/ +export function array( elementSchema: TSchema, length: number, ): ArraySchema { diff --git a/packages/typed-binary/src/structure/baseTypes.ts b/packages/typed-binary/src/structure/baseTypes.ts index af80f2e..17d0a29 100644 --- a/packages/typed-binary/src/structure/baseTypes.ts +++ b/packages/typed-binary/src/structure/baseTypes.ts @@ -6,7 +6,9 @@ import { MaxValue, Schema } from './types.ts'; // BOOL //// -export class BoolSchema extends Schema { +export interface Bool extends Schema {} + +class BoolSchema implements Bool { /** * The maximum number of bytes this schema can take up. * @@ -27,13 +29,18 @@ export class BoolSchema extends Schema { } } -export const bool: BoolSchema = new BoolSchema(); +export const bool: Bool = new BoolSchema(); //// // STRING //// -export class StringSchema extends Schema { +export interface String extends Schema {} + +class StringSchema implements String { + declare readonly $in: string; + declare readonly $out: string; + private static _cachedEncoder: TextEncoder | undefined; private static get _encoder() { @@ -61,13 +68,15 @@ export class StringSchema extends Schema { } } -export const string: StringSchema = new StringSchema(); +export const string: String = new StringSchema(); //// // i8 //// -export class Int8Schema extends Schema { +export interface Int8 extends Schema {} + +class Int8Schema implements Int8 { /** * The maximum number of bytes this schema can take up. * @@ -88,13 +97,15 @@ export class Int8Schema extends Schema { } } -export const i8: Int8Schema = new Int8Schema(); +export const i8: Int8 = new Int8Schema(); //// // u8 //// -export class Uint8Schema extends Schema { +export interface Uint8 extends Schema {} + +class Uint8Schema implements Uint8 { /** * The maximum number of bytes this schema can take up. * @@ -115,18 +126,15 @@ export class Uint8Schema extends Schema { } } -export const u8: Uint8Schema = new Uint8Schema(); - -/** - * Alias for `bin.u8` - */ -export const byte: Uint8Schema = u8; +export const u8: Uint8 = new Uint8Schema(); //// // i16 //// -export class Int16Schema extends Schema { +export interface Int16 extends Schema {} + +class Int16Schema implements Int16 { /** * The maximum number of bytes this schema can take up. * @@ -147,13 +155,15 @@ export class Int16Schema extends Schema { } } -export const i16: Int16Schema = new Int16Schema(); +export const i16: Int16 = new Int16Schema(); //// // u16 //// -export class Uint16Schema extends Schema { +export interface Uint16 extends Schema {} + +class Uint16Schema implements Uint16 { /** * The maximum number of bytes this schema can take up. * @@ -174,13 +184,18 @@ export class Uint16Schema extends Schema { } } -export const u16: Uint16Schema = new Uint16Schema(); +export const u16: Uint16 = new Uint16Schema(); //// // i32 //// -export class Int32Schema extends Schema { +export interface Int32 extends Schema {} + +class Int32Schema implements Int32 { + declare readonly $in: number; + declare readonly $out: number; + /** * The maximum number of bytes this schema can take up. * @@ -201,13 +216,15 @@ export class Int32Schema extends Schema { } } -export const i32: Int32Schema = new Int32Schema(); +export const i32: Int32 = new Int32Schema(); //// // u32 //// -export class Uint32Schema extends Schema { +export interface Uint32 extends Schema {} + +class Uint32Schema implements Uint32 { /** * The maximum number of bytes this schema can take up. * @@ -228,13 +245,15 @@ export class Uint32Schema extends Schema { } } -export const u32: Uint32Schema = new Uint32Schema(); +export const u32: Uint32 = new Uint32Schema(); //// // f16 //// -export class Float16Schema extends Schema { +export interface Float16 extends Schema {} + +class Float16Schema implements Float16 { /** * The maximum number of bytes this schema can take up. * @@ -255,13 +274,15 @@ export class Float16Schema extends Schema { } } -export const f16: Float16Schema = new Float16Schema(); +export const f16: Float16 = new Float16Schema(); //// // f32 //// -export class Float32Schema extends Schema { +export interface Float32 extends Schema {} + +class Float32Schema implements Float32 { /** * The maximum number of bytes this schema can take up. * @@ -282,4 +303,4 @@ export class Float32Schema extends Schema { } } -export const f32: Float32Schema = new Float32Schema(); +export const f32: Float32 = new Float32Schema(); diff --git a/packages/typed-binary/src/structure/chars.ts b/packages/typed-binary/src/structure/chars.ts index aef6702..8be38cb 100644 --- a/packages/typed-binary/src/structure/chars.ts +++ b/packages/typed-binary/src/structure/chars.ts @@ -3,9 +3,15 @@ import { Measurer } from '../io/measurer.ts'; import type { IMeasurer, ISerialInput, ISerialOutput } from '../io/types.ts'; import { Schema } from './types.ts'; -export class CharsSchema extends Schema { - constructor(public readonly length: TLength) { - super(); +export interface Chars extends Schema { + readonly length: TLength; +} + +class CharsSchema implements Chars { + readonly length: TLength; + + constructor(length: TLength) { + this.length = length; } write(output: ISerialOutput, value: string): void { @@ -24,7 +30,7 @@ export class CharsSchema extends Schema let content = ''; for (let i = 0; i < this.length; ++i) { - content += String.fromCharCode(input.readByte()); + content += String.fromCharCode(input.readUint8()); } return content; @@ -35,7 +41,7 @@ export class CharsSchema extends Schema } } -// @__NO_SIDE_EFFECTS__ -export function chars(length: T): CharsSchema { +/*#__NO_SIDE_EFFECTS__*/ +export function chars(length: T): Chars { return new CharsSchema(length); } diff --git a/packages/typed-binary/src/structure/concat.ts b/packages/typed-binary/src/structure/concat.ts index 0920102..e41311c 100644 --- a/packages/typed-binary/src/structure/concat.ts +++ b/packages/typed-binary/src/structure/concat.ts @@ -1,5 +1,5 @@ import type { MergeRecordUnion } from '../utilityTypes.ts'; -import { type AnyObjectSchema, ObjectSchema } from './object.ts'; +import { type AnyObjectSchema, ObjectSchema } from './struct.ts'; import type { PropertiesOf } from './types.ts'; type Concat = ObjectSchema< diff --git a/packages/typed-binary/src/structure/dynamicArray.ts b/packages/typed-binary/src/structure/dynamicArray.ts index cc19eba..b663739 100644 --- a/packages/typed-binary/src/structure/dynamicArray.ts +++ b/packages/typed-binary/src/structure/dynamicArray.ts @@ -1,31 +1,26 @@ import { Measurer } from '../io/measurer.ts'; import type { IMeasurer, ISerialInput, ISerialOutput } from '../io/types.ts'; -import type { ParseUnwrapped } from '../utilityTypes.ts'; import { - type AnySchema, - type IRefResolver, + type ExtractIn, + type ExtractOut, MaxValue, type PropertyDescription, - Schema, - type Unwrap, + type Schema, } from './types.ts'; -export class DynamicArraySchema extends Schema[]> { - public elementType: TElement; +export interface DynamicArray extends Schema< + readonly ExtractIn[], + ExtractOut[] +> {} - constructor(private readonly _unstableElementType: TElement) { - super(); +class DynamicArraySchema implements DynamicArray { + readonly elementType: TElement; - // In case this array isn't part of a keyed chain, - // let's assume the inner type is stable. - this.elementType = _unstableElementType; + constructor(elementType: TElement) { + this.elementType = elementType; } - override resolveReferences(ctx: IRefResolver): void { - this.elementType = ctx.resolve(this._unstableElementType); - } - - override write(output: ISerialOutput, values: ParseUnwrapped[]): void { + write(output: ISerialOutput, values: readonly ExtractIn[]): void { output.writeUint32(values.length); for (const value of values) { @@ -33,13 +28,13 @@ export class DynamicArraySchema extends Schema[] { - const array: ParseUnwrapped[] = []; + read(input: ISerialInput): ExtractOut[] { + const array: ExtractOut[] = []; const len = input.readUint32(); for (let i = 0; i < len; ++i) { - array.push(this.elementType.read(input) as ParseUnwrapped); + array.push(this.elementType.read(input)); } return array; @@ -57,8 +52,8 @@ export class DynamicArraySchema extends Schema[] | typeof MaxValue, + measure( + values: readonly ExtractIn[] | typeof MaxValue, measurer: IMeasurer = new Measurer(), ): IMeasurer { if (values === MaxValue) { @@ -76,47 +71,9 @@ export class DynamicArraySchema extends Schema[] | MaxValue, - prop: number, - ): PropertyDescription | null { - if (typeof prop === 'symbol') { - return null; - } - - const indexProp = Number.parseInt(String(prop), 10); - if (Number.isNaN(indexProp)) { - return null; - } - - if (reference === MaxValue) { - return { - bufferOffset: this.elementType.measure(MaxValue).size * indexProp, - schema: this.elementType, - }; - } - - if (indexProp >= reference.length) { - // index out of range - return null; - } - - const measurer = new Measurer(); - for (let i = 0; i < indexProp; ++i) { - this.elementType.measure(reference[i], measurer); - } - - return { - bufferOffset: measurer.size, - schema: this.elementType, - }; - } } -// @__NO_SIDE_EFFECTS__ -export function dynamicArrayOf( - elementSchema: TSchema, -): DynamicArraySchema { +/*@__NO_SIDE_EFFECTS__*/ +export function dynamicArray(elementSchema: TSchema): DynamicArray { return new DynamicArraySchema(elementSchema); } diff --git a/packages/typed-binary/src/structure/genericObject.ts b/packages/typed-binary/src/structure/genericObject.ts new file mode 100644 index 0000000..67bce03 --- /dev/null +++ b/packages/typed-binary/src/structure/genericObject.ts @@ -0,0 +1,159 @@ +import { ISerialOutput } from '../main-api.ts'; +import { ObjectSchema } from './struct.ts'; +import { SubTypeKey, type ExtractInRecord, type ExtractOutRecord, type Schema } from './types.ts'; + +type TIn, Ext> = { + [TKey in keyof Ext]: Schema< + Readonly & { type: TKey } & ExtractInRecord> + >; +}[keyof Ext]; + +type TOut, Ext> = { + [TKey in keyof Ext]: Schema< + ExtractOutRecord & { type: TKey } & ExtractOutRecord + >; +}[keyof Ext]; + +export class GenericObjectSchema< + TBase extends Record, // Base properties + TExt extends Record>, // Sub type map +> implements Schema, TOut> { + readonly keyedBy: SubTypeKey; + #baseObject: ObjectSchema; + public subTypeMap: TExt; + + constructor(keyedBy: SubTypeKey, properties: TBase, subTypeMap: TExt) { + this.keyedBy = keyedBy; + this.subTypeMap = subTypeMap; + this.#baseObject = new ObjectSchema(properties); + } + + write(output: ISerialOutput, value: TIn): void { + // Figuring out sub-types + + const subTypeKey = value.type as keyof TUnwrapExt; + const subTypeDescription = this.subTypeMap[subTypeKey] || null; + if (subTypeDescription === null) { + throw new Error( + `Unknown sub-type '${subTypeKey.toString()}' in among '${JSON.stringify( + Object.keys(this.subTypeMap), + )}'`, + ); + } + + // Writing the sub-type out. + if (this.keyedBy === SubTypeKey.ENUM) { + output.writeUint8(value.type as number); + } else { + output.writeString(value.type as string); + } + + // Writing the base properties + this._baseObject.write(output, value as ParseUnwrappedRecord); + + // Extra sub-type fields + for (const [key, extraProp] of exactEntries(subTypeDescription.properties)) { + extraProp.write(output, value[key]); + } + } + + override read(input: ISerialInput): Parsed> { + const subTypeKey = this.keyedBy === SubTypeKey.ENUM ? input.readByte() : input.readString(); + + const subTypeDescription = this.subTypeMap[subTypeKey as keyof TUnwrapExt] || null; + if (subTypeDescription === null) { + throw new Error( + `Unknown sub-type '${subTypeKey}' in among '${JSON.stringify( + Object.keys(this.subTypeMap), + )}'`, + ); + } + + const result = this._baseObject.read(input) as Parsed>; + + // Making the sub type key available to the result object. + (result as { type: keyof TUnwrapExt }).type = subTypeKey as keyof TUnwrapExt; + + if (subTypeDescription !== null) { + for (const [key, extraProp] of exactEntries(subTypeDescription.properties)) { + (result as any)[key] = extraProp.read(input); + } + } + + return result; + } + + measure( + value: Parsed> | MaxValue, + measurer: IMeasurer = new Measurer(), + ): IMeasurer { + this._baseObject.measure(value as Parsed> | MaxValue, measurer); + + // We're a generic object trying to encode a concrete value. + if (this.keyedBy === SubTypeKey.ENUM) { + measurer.add(1); + } else if (value !== MaxValue) { + measurer.add((value.type as string).length + 1); + } else { + // 'type' can be a string of any length, so the schema is unbounded. + return measurer.unbounded; + } + + // Extra sub-type fields + if (value === MaxValue) { + const biggestSubType = (Object.values(this.subTypeMap) as TUnwrapExt[keyof TUnwrapExt][]) + .map((subType) => { + const forkedMeasurer = measurer.fork(); + + // Going through extra properties + for (const prop of Object.values(subType.properties)) { + // Measuring them + prop.measure(MaxValue, forkedMeasurer); + } + + return [subType, forkedMeasurer.size] as const; + }) + .reduce((a, b) => (a[1] > b[1] ? a : b))[0]; + + // Going through extra properties + for (const prop of Object.values(biggestSubType.properties)) { + // Measuring for real this time + prop.measure(MaxValue, measurer); + } + } else { + const subTypeKey = (value as { type: keyof TUnwrapExt }).type; + const subTypeDescription = this.subTypeMap[subTypeKey] || null; + if (subTypeDescription === null) { + throw new Error( + `Unknown sub-type '${subTypeKey.toString()}', expected one of '${JSON.stringify( + Object.keys(this.subTypeMap), + )}'`, + ); + } + + // Going through extra properties + for (const [key, prop] of exactEntries(subTypeDescription.properties)) { + // Measuring them + prop.measure(value[key], measurer); + } + } + + return measurer; + } +} + +/*#__NO_SIDE_EFFECTS__*/ +export function generic< + P extends Record, + S extends Record>, +>(properties: P, subTypeMap: S): GenericObjectSchema { + return new GenericObjectSchema(SubTypeKey.STRING, properties, subTypeMap); +} + +/*#__NO_SIDE_EFFECTS__*/ +export function genericEnum< + P extends Record, + S extends Record>, +>(properties: P, subTypeMap: S): GenericObjectSchema { + return new GenericObjectSchema(SubTypeKey.ENUM, properties, subTypeMap); +} diff --git a/packages/typed-binary/src/structure/keyed.ts b/packages/typed-binary/src/structure/keyed.ts deleted file mode 100644 index 20c6e68..0000000 --- a/packages/typed-binary/src/structure/keyed.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { UnresolvedReferenceError } from '../error.ts'; -import { Measurer } from '../io/measurer.ts'; -import type { IMeasurer, ISerialInput, ISerialOutput } from '../io/types.ts'; -import type { ParseUnwrapped, Parsed } from '../utilityTypes.ts'; -import { - type AnySchema, - type IKeyedSchema, - type IRefResolver, - type ISchema, - MaxValue, - type PropertyDescription, - Ref, - type Unwrap, -} from './types.ts'; - -class RefSchema implements ISchema> { - public readonly __unwrapped!: Ref; - public readonly ref: Ref; - - constructor(key: TKeyDef) { - this.ref = new Ref(key); - } - - resolveReferences(): void { - throw new UnresolvedReferenceError( - 'Tried to resolve a reference directly. Do it through a RefResolver instead.', - ); - } - - read(): Parsed> { - throw new UnresolvedReferenceError('Tried to read a reference directly. Resolve it instead.'); - } - - write(): void { - throw new UnresolvedReferenceError('Tried to write a reference directly. Resolve it instead.'); - } - - measure(): IMeasurer { - throw new UnresolvedReferenceError( - 'Tried to measure size of a reference directly. Resolve it instead.', - ); - } - - seekProperty(): PropertyDescription | null { - throw new UnresolvedReferenceError( - 'Tried to seek property of a reference directly. Resolve it instead.', - ); - } -} - -class RefResolve implements IRefResolver { - private registry: { [key: string]: ISchema } = {}; - - hasKey(key: string): boolean { - return this.registry[key] !== undefined; - } - - register(key: K, schema: ISchema): void { - this.registry[key] = schema; - } - - resolve(unstableSchema: TSchema): TSchema { - if (unstableSchema instanceof RefSchema) { - const ref = unstableSchema.ref; - const key = ref.key as string; - if (this.registry[key] !== undefined) { - return this.registry[key] as TSchema; - } - - throw new UnresolvedReferenceError(`Couldn't resolve reference to ${key}. Unknown key.`); - } - - // Since it's not a RefSchema, we assume it can be resolved. - unstableSchema.resolveReferences(this); - - return unstableSchema; - } -} - -export class KeyedSchema< - TInner extends ISchema, - TKeyDef extends string, -> implements IKeyedSchema> { - public readonly __unwrapped!: Unwrap; - public readonly __keyDefinition!: TKeyDef; - public innerType: TInner; - - constructor( - public readonly key: TKeyDef, - innerResolver: (ref: ISchema>) => TInner, - ) { - this.innerType = innerResolver(new RefSchema(key)); - - // Automatically resolving after keyed creation. - this.resolveReferences(new RefResolve()); - } - - resolveReferences(ctx: IRefResolver): void { - if (!ctx.hasKey(this.key)) { - ctx.register(this.key, this.innerType); - - this.innerType.resolveReferences(ctx); - } - } - - read(input: ISerialInput): ParseUnwrapped { - return this.innerType.read(input) as ParseUnwrapped; - } - - write(output: ISerialOutput, value: ParseUnwrapped): void { - this.innerType.write(output, value); - } - - /** - * The maximum number of bytes this schema can take up. - * - * Is `NaN` if the schema is unbounded. If you would like to know - * how many bytes a particular value encoding will take up, use `.measure(value)`. - * - * Alias for `.measure(MaxValue).size` - */ - get maxSize(): number { - return this.measure(MaxValue).size; - } - - measure( - value: ParseUnwrapped | typeof MaxValue, - measurer: IMeasurer = new Measurer(), - ): IMeasurer { - return this.innerType.measure(value, measurer); - } - - seekProperty( - reference: ParseUnwrapped | typeof MaxValue, - prop: keyof Unwrap, - ): PropertyDescription | null { - return this.innerType.seekProperty(reference, prop as never); - } -} - -// @__NO_SIDE_EFFECTS__ -export function keyed>( - key: K, - inner: (ref: ISchema>) => P, -): KeyedSchema { - return new KeyedSchema(key, inner); -} diff --git a/packages/typed-binary/src/structure/object.ts b/packages/typed-binary/src/structure/object.ts deleted file mode 100644 index 5133d44..0000000 --- a/packages/typed-binary/src/structure/object.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { Measurer } from '../io/measurer.ts'; -import type { IMeasurer, ISerialInput, ISerialOutput } from '../io/types.ts'; -import type { ParseUnwrappedRecord, Parsed } from '../utilityTypes.ts'; -import { - type AnySchema, - type AnySchemaWithProperties, - type IRefResolver, - type ISchema, - type ISchemaWithProperties, - MaxValue, - type PropertyDescription, - Schema, - SubTypeKey, - type Unwrap, - type UnwrapRecord, -} from './types.ts'; - -// @__NO_SIDE_EFFECTS__ -export function exactEntries>( - record: T, -): [keyof T, T[keyof T]][] { - return Object.entries(record) as [keyof T, T[keyof T]][]; -} - -// @__NO_SIDE_EFFECTS__ -export function resolveMap>(ctx: IRefResolver, refs: T): T { - const props = {} as T; - - for (const [key, ref] of exactEntries(refs)) { - props[key] = ctx.resolve(ref); - } - - return props; -} - -export type AnyObjectSchema = ObjectSchema>; - -export class ObjectSchema> - extends Schema> - implements ISchemaWithProperties -{ - public properties: TProps; - - constructor(private readonly _properties: TProps) { - super(); - - // In case this object isn't part of a keyed chain, - // let's assume properties are stable. - this.properties = _properties; - } - - override resolveReferences(ctx: IRefResolver): void { - this.properties = resolveMap(ctx, this._properties); - } - - override write(output: ISerialOutput, value: ParseUnwrappedRecord): void { - type Property = keyof ParseUnwrappedRecord; - - for (const [key, property] of exactEntries(this.properties)) { - property.write(output, value[key as Property]); - } - } - - override read(input: ISerialInput): ParseUnwrappedRecord { - type Property = keyof ParseUnwrappedRecord; - - const result = {} as ParseUnwrappedRecord; - - for (const [key, property] of exactEntries(this.properties)) { - result[key as Property] = property.read(input) as Parsed>[Property]; - } - - return result; - } - - /** - * The maximum number of bytes this schema can take up. - * - * Is `NaN` if the schema is unbounded. If you would like to know - * how many bytes a particular value encoding will take up, use `.measure(value)`. - * - * Alias for `.measure(MaxValue).size` - */ - get maxSize(): number { - const measurer = new Measurer(); - - for (const property of Object.values(this.properties)) { - property.measure(MaxValue, measurer); - } - - return measurer.size; - } - - override measure( - value: ParseUnwrappedRecord | typeof MaxValue, - measurer: IMeasurer = new Measurer(), - ): IMeasurer { - type Property = keyof ParseUnwrappedRecord; - - for (const [key, property] of exactEntries(this.properties)) { - property.measure(value === MaxValue ? MaxValue : value[key as Property], measurer); - } - - return measurer; - } - - override seekProperty( - reference: ParseUnwrappedRecord | MaxValue, - prop: keyof UnwrapRecord, - ): PropertyDescription | null { - let bufferOffset = 0; - - for (const [key, property] of exactEntries(this.properties)) { - if (key === prop) { - return { - bufferOffset, - schema: property, - }; - } - - bufferOffset += property.measure(reference).size; - } - - return null; - } -} - -// @__NO_SIDE_EFFECTS__ -export function object

>(properties: P): ObjectSchema

{ - return new ObjectSchema(properties); -} - -type UnwrapGeneric, Ext> = { - [TKey in keyof Ext]: ISchema< - UnwrapRecord & { type: TKey } & UnwrapRecord> - >; -}[keyof Ext]; - -export class GenericObjectSchema< - TUnwrapBase extends Record, // Base properties - TUnwrapExt extends Record, // Sub type map -> extends Schema> { - private _baseObject: ObjectSchema; - public subTypeMap: TUnwrapExt; - - constructor( - public readonly keyedBy: SubTypeKey, - properties: TUnwrapBase, - private readonly _subTypeMap: TUnwrapExt, - ) { - super(); - - this._baseObject = new ObjectSchema(properties); - - // In case this object isn't part of a keyed chain, - // let's assume sub types are stable. - this.subTypeMap = _subTypeMap; - } - - override resolveReferences(ctx: IRefResolver): void { - this._baseObject.resolveReferences(ctx); - this.subTypeMap = resolveMap(ctx, this._subTypeMap); - } - - override write( - output: ISerialOutput, - value: Parsed>, - ): void { - // Figuring out sub-types - - const subTypeKey = value.type as keyof TUnwrapExt; - const subTypeDescription = this.subTypeMap[subTypeKey] || null; - if (subTypeDescription === null) { - throw new Error( - `Unknown sub-type '${subTypeKey.toString()}' in among '${JSON.stringify( - Object.keys(this.subTypeMap), - )}'`, - ); - } - - // Writing the sub-type out. - if (this.keyedBy === SubTypeKey.ENUM) { - output.writeUint8(value.type as number); - } else { - output.writeString(value.type as string); - } - - // Writing the base properties - this._baseObject.write(output, value as ParseUnwrappedRecord); - - // Extra sub-type fields - for (const [key, extraProp] of exactEntries(subTypeDescription.properties)) { - extraProp.write(output, value[key]); - } - } - - override read(input: ISerialInput): Parsed> { - const subTypeKey = this.keyedBy === SubTypeKey.ENUM ? input.readByte() : input.readString(); - - const subTypeDescription = this.subTypeMap[subTypeKey as keyof TUnwrapExt] || null; - if (subTypeDescription === null) { - throw new Error( - `Unknown sub-type '${subTypeKey}' in among '${JSON.stringify( - Object.keys(this.subTypeMap), - )}'`, - ); - } - - const result = this._baseObject.read(input) as Parsed>; - - // Making the sub type key available to the result object. - (result as { type: keyof TUnwrapExt }).type = subTypeKey as keyof TUnwrapExt; - - if (subTypeDescription !== null) { - for (const [key, extraProp] of exactEntries(subTypeDescription.properties)) { - (result as any)[key] = extraProp.read(input); - } - } - - return result; - } - - measure( - value: Parsed> | MaxValue, - measurer: IMeasurer = new Measurer(), - ): IMeasurer { - this._baseObject.measure(value as Parsed> | MaxValue, measurer); - - // We're a generic object trying to encode a concrete value. - if (this.keyedBy === SubTypeKey.ENUM) { - measurer.add(1); - } else if (value !== MaxValue) { - measurer.add((value.type as string).length + 1); - } else { - // 'type' can be a string of any length, so the schema is unbounded. - return measurer.unbounded; - } - - // Extra sub-type fields - if (value === MaxValue) { - const biggestSubType = (Object.values(this.subTypeMap) as TUnwrapExt[keyof TUnwrapExt][]) - .map((subType) => { - const forkedMeasurer = measurer.fork(); - - // Going through extra properties - for (const prop of Object.values(subType.properties)) { - // Measuring them - prop.measure(MaxValue, forkedMeasurer); - } - - return [subType, forkedMeasurer.size] as const; - }) - .reduce((a, b) => (a[1] > b[1] ? a : b))[0]; - - // Going through extra properties - for (const prop of Object.values(biggestSubType.properties)) { - // Measuring for real this time - prop.measure(MaxValue, measurer); - } - } else { - const subTypeKey = (value as { type: keyof TUnwrapExt }).type; - const subTypeDescription = this.subTypeMap[subTypeKey] || null; - if (subTypeDescription === null) { - throw new Error( - `Unknown sub-type '${subTypeKey.toString()}', expected one of '${JSON.stringify( - Object.keys(this.subTypeMap), - )}'`, - ); - } - - // Going through extra properties - for (const [key, prop] of exactEntries(subTypeDescription.properties)) { - // Measuring them - prop.measure(value[key], measurer); - } - } - - return measurer; - } -} - -// @__NO_SIDE_EFFECTS__ -export function generic< - P extends Record, - S extends { - [Key in keyof S]: AnySchemaWithProperties; - }, ->(properties: P, subTypeMap: S): GenericObjectSchema { - return new GenericObjectSchema(SubTypeKey.STRING, properties, subTypeMap); -} - -// @__NO_SIDE_EFFECTS__ -export function genericEnum< - P extends Record, - S extends { - [Key in keyof S]: AnySchemaWithProperties; - }, ->(properties: P, subTypeMap: S): GenericObjectSchema { - return new GenericObjectSchema(SubTypeKey.ENUM, properties, subTypeMap); -} diff --git a/packages/typed-binary/src/structure/optional.ts b/packages/typed-binary/src/structure/optional.ts index 7e0a6c4..f57ba10 100644 --- a/packages/typed-binary/src/structure/optional.ts +++ b/packages/typed-binary/src/structure/optional.ts @@ -1,37 +1,35 @@ import { Measurer } from '../io/measurer.ts'; import type { IMeasurer, ISerialInput, ISerialOutput } from '../io/types.ts'; -import type { ParseUnwrapped } from '../utilityTypes.ts'; -import { type AnySchema, type IRefResolver, MaxValue, Schema, type Unwrap } from './types.ts'; +import { type ExtractIn, type ExtractOut, MaxValue, type Schema } from './types.ts'; -export class OptionalSchema extends Schema | undefined> { - private innerSchema: TInner; +export interface Optional extends Schema< + ExtractIn | undefined, + ExtractOut | undefined +> {} - constructor(private readonly _innerUnstableSchema: TInner) { - super(); +class OptionalSchemaImpl implements Optional { + declare readonly $in: ExtractIn | undefined; + declare readonly $out: ExtractOut | undefined; + readonly inner: TInner & Schema; - // In case this optional isn't part of a keyed chain, - // let's assume the inner type is stable. - this.innerSchema = _innerUnstableSchema; + constructor(inner: TInner) { + this.inner = inner as TInner & Schema; } - override resolveReferences(ctx: IRefResolver): void { - this.innerSchema = ctx.resolve(this._innerUnstableSchema); - } - - override write(output: ISerialOutput, value: ParseUnwrapped | undefined): void { + write(output: ISerialOutput, value: ExtractIn | undefined): void { if (value !== undefined && value !== null) { output.writeBool(true); - this.innerSchema.write(output, value); + this.inner.write(output, value); } else { output.writeBool(false); } } - override read(input: ISerialInput): ParseUnwrapped | undefined { + read(input: ISerialInput): ExtractOut | undefined { const valueExists = input.readBool(); if (valueExists) { - return this.innerSchema.read(input) as ParseUnwrapped; + return this.inner.read(input); } return undefined; @@ -49,19 +47,19 @@ export class OptionalSchema extends Schema | MaxValue | undefined, + measure( + value: ExtractIn | MaxValue | undefined, measurer: IMeasurer = new Measurer(), ): IMeasurer { if (value !== undefined) { - this.innerSchema.measure(value, measurer); + this.inner.measure(value, measurer); } return measurer.add(1); } } -// @__NO_SIDE_EFFECTS__ -export function optional(innerType: TSchema): OptionalSchema { - return new OptionalSchema(innerType); +/*#__NO_SIDE_EFFECTS__*/ +export function optional(innerType: TSchema): Optional { + return new OptionalSchemaImpl(innerType); } diff --git a/packages/typed-binary/src/structure/struct.ts b/packages/typed-binary/src/structure/struct.ts new file mode 100644 index 0000000..dc657dd --- /dev/null +++ b/packages/typed-binary/src/structure/struct.ts @@ -0,0 +1,111 @@ +import { Measurer } from '../io/measurer.ts'; +import type { IMeasurer, ISerialInput, ISerialOutput } from '../io/types.ts'; +import { MutableRecord, Prettify } from '../utilityTypes.ts'; +import { + type ExtractIn, + type ExtractInRecord, + type ExtractOutRecord, + MaxValue, + type PropertyDescription, + type Schema, +} from './types.ts'; + +/*#__NO_SIDE_EFFECTS__*/ +export function exactEntries>( + record: T, +): [keyof T, T[keyof T]][] { + return Object.entries(record) as [keyof T, T[keyof T]][]; +} + +type TIn> = Readonly>; +type TOut> = ExtractOutRecord; + +export interface Struct> extends Schema< + TIn, + TOut +> { + readonly properties: TProps; +} + +class ObjectSchema> implements Struct { + readonly properties: TProps; + + constructor(properties: TProps) { + this.properties = properties; + } + + write(output: ISerialOutput, value: TIn): void { + type Property = keyof TIn; + + for (const [key, property] of exactEntries(this.properties)) { + property.write(output, value[key as Property] as ExtractIn); + } + } + + read(input: ISerialInput) { + type Property = keyof TOut; + + const result = {} as TOut; + + for (const [key, property] of exactEntries(this.properties)) { + result[key as Property] = property.read(input) as TOut[Property]; + } + + return result; + } + + /** + * The maximum number of bytes this schema can take up. + * + * Is `NaN` if the schema is unbounded. If you would like to know + * how many bytes a particular value encoding will take up, use `.measure(value)`. + * + * Alias for `.measure(MaxValue).size` + */ + get maxSize(): number { + const measurer = new Measurer(); + + for (const property of Object.values(this.properties)) { + property.measure(MaxValue, measurer); + } + + return measurer.size; + } + + measure(value: TIn | typeof MaxValue, measurer: IMeasurer = new Measurer()): IMeasurer { + type Property = keyof TIn; + + for (const [key, property] of exactEntries(this.properties)) { + property.measure(value === MaxValue ? MaxValue : value[key as Property], measurer); + } + + return measurer; + } + + seekProperty( + reference: TIn | MaxValue, + prop: keyof TIn, + ): PropertyDescription | undefined { + let bufferOffset = 0; + + for (const [key, property] of exactEntries(this.properties)) { + if (key === prop) { + return { + bufferOffset, + schema: property, + }; + } + + bufferOffset += property.measure(reference).size; + } + + return undefined; + } +} + +/*#__NO_SIDE_EFFECTS__*/ +export function struct

( + properties: P, +): Struct>> { + return new ObjectSchema(properties); +} diff --git a/packages/typed-binary/src/structure/tuple.ts b/packages/typed-binary/src/structure/tuple.ts index cab220a..afee9bc 100644 --- a/packages/typed-binary/src/structure/tuple.ts +++ b/packages/typed-binary/src/structure/tuple.ts @@ -1,32 +1,36 @@ import { ValidationError } from '../error.ts'; import { Measurer } from '../io/measurer.ts'; import type { IMeasurer, ISerialInput, ISerialOutput } from '../io/types.ts'; -import type { Parsed } from '../utilityTypes.ts'; -import { type AnySchema, type IRefResolver, MaxValue, Schema, type UnwrapArray } from './types.ts'; +import { MaxValue, type Schema, ExtractInRecord, ExtractOutRecord } from './types.ts'; -// @__NO_SIDE_EFFECTS__ -export function resolveArray(ctx: IRefResolver, refs: T): T { - return refs.map((ref) => ctx.resolve(ref)) as T; -} - -export class TupleSchema extends Schema< - UnwrapArray +export interface Tuple extends Schema< + ExtractInRecord>, + ExtractOutRecord > { - private schemas: TSequence; + readonly schemas: TSequence; - constructor(private readonly _unstableSchemas: TSequence) { - super(); + /** + * The maximum number of bytes this schema can take up. + * + * Is `NaN` if the schema is unbounded. If you would like to know + * how many bytes a particular value encoding will take up, use `.measure(value)`. + * + * Alias for `.measure(MaxValue).size` + */ + readonly maxSize: number; +} - // In case this tuple isn't part of a keyed chain, - // let's assume the inner type is stable. - this.schemas = _unstableSchemas; - } +class TupleSchema implements Schema< + ExtractInRecord, + ExtractOutRecord +> { + readonly schemas: TSequence; - override resolveReferences(ctx: IRefResolver): void { - this.schemas = resolveArray(ctx, this._unstableSchemas); + constructor(schemas: TSequence) { + this.schemas = schemas; } - override write(output: ISerialOutput, values: Parsed>): void { + write(output: ISerialOutput, values: ExtractInRecord): void { if (values.length !== this.schemas.length) { throw new ValidationError( `Expected tuple of length ${this.schemas.length}, got ${values.length}`, @@ -38,30 +42,18 @@ export class TupleSchema extends } } - override read(input: ISerialInput): Parsed> { - const array = [] as Parsed>; - - for (let i = 0; i < this.schemas.length; ++i) { - array.push(this.schemas[i].read(input) as Parsed>[number]); - } - - return array; + read(input: ISerialInput): ExtractOutRecord { + return this.schemas.map((schema) => + schema.read(input), + ) as unknown as ExtractOutRecord; } - /** - * The maximum number of bytes this schema can take up. - * - * Is `NaN` if the schema is unbounded. If you would like to know - * how many bytes a particular value encoding will take up, use `.measure(value)`. - * - * Alias for `.measure(MaxValue).size` - */ get maxSize(): number { return this.measure(MaxValue).size; } measure( - values: Parsed> | MaxValue, + values: ExtractInRecord | MaxValue, measurer: IMeasurer = new Measurer(), ): IMeasurer { for (let i = 0; i < this.schemas.length; ++i) { @@ -72,9 +64,7 @@ export class TupleSchema extends } } -// @__NO_SIDE_EFFECTS__ -export function tupleOf( - schemas: TSchema, -): TupleSchema { +/*#__NO_SIDE_EFFECTS__*/ +export function tuple(schemas: TSchema): Tuple { return new TupleSchema(schemas); } diff --git a/packages/typed-binary/src/structure/typedArray.ts b/packages/typed-binary/src/structure/typedArray.ts index af58164..9cfc22e 100644 --- a/packages/typed-binary/src/structure/typedArray.ts +++ b/packages/typed-binary/src/structure/typedArray.ts @@ -1,6 +1,5 @@ import { Measurer } from '../io/measurer.ts'; import type { IMeasurer, ISerialInput, ISerialOutput } from '../io/types.ts'; -import type { Parsed } from '../utilityTypes.ts'; import { type MaxValue, Schema } from './types.ts'; type TypedArrayConstructor = { @@ -10,69 +9,69 @@ type TypedArrayConstructor = { export class TypedArraySchema< TTypedArray extends ArrayLike & ArrayBufferView, -> extends Schema { - public readonly byteLength: number; +> implements Schema { + declare readonly $in: TTypedArray; + declare readonly $out: TTypedArray; - constructor( - public readonly length: number, - private readonly _arrayConstructor: TypedArrayConstructor, - ) { - super(); + readonly byteLength: number; + readonly elementCount: number; - this.byteLength = length * _arrayConstructor.BYTES_PER_ELEMENT; + readonly #arrayConstructor: TypedArrayConstructor; + + constructor(elementCount: number, arrayConstructor: TypedArrayConstructor) { + this.elementCount = elementCount; + this.byteLength = length * arrayConstructor.BYTES_PER_ELEMENT; + this.#arrayConstructor = arrayConstructor; } - write(output: ISerialOutput, value: Parsed): void { + write(output: ISerialOutput, value: TTypedArray): void { output.writeSlice(value); } - read(input: ISerialInput): Parsed { + read(input: ISerialInput): TTypedArray { const buffer = new ArrayBuffer(this.byteLength); - const view = new this._arrayConstructor(buffer, 0, this.length); + const view = new this.#arrayConstructor(buffer, 0, this.elementCount); input.readSlice(view, 0, this.byteLength); - return view as Parsed; + return view; } - measure( - _value: Parsed | typeof MaxValue, - measurer: IMeasurer = new Measurer(), - ): IMeasurer { + measure(_value: TTypedArray | typeof MaxValue, measurer: IMeasurer = new Measurer()): IMeasurer { return measurer.add(this.byteLength); } } -// @__NO_SIDE_EFFECTS__ +/*#__NO_SIDE_EFFECTS__*/ export const u8Array = (length: number): TypedArraySchema => new TypedArraySchema(length, Uint8Array); -// @__NO_SIDE_EFFECTS__ +/*#__NO_SIDE_EFFECTS__*/ export const u8ClampedArray = (length: number): TypedArraySchema => new TypedArraySchema(length, Uint8ClampedArray); -// @__NO_SIDE_EFFECTS__ +/*#__NO_SIDE_EFFECTS__*/ export const u16Array = (length: number): TypedArraySchema => new TypedArraySchema(length, Uint16Array); -// @__NO_SIDE_EFFECTS__ +/*#__NO_SIDE_EFFECTS__*/ export const u32Array = (length: number): TypedArraySchema => new TypedArraySchema(length, Uint32Array); -// @__NO_SIDE_EFFECTS__ +/*#__NO_SIDE_EFFECTS__*/ export const i8Array = (length: number): TypedArraySchema => new TypedArraySchema(length, Int8Array); -// @__NO_SIDE_EFFECTS__ +/*#__NO_SIDE_EFFECTS__*/ export const i16Array = (length: number): TypedArraySchema => new TypedArraySchema(length, Int16Array); -// @__NO_SIDE_EFFECTS__ +/*#__NO_SIDE_EFFECTS__*/ export const i32Array = (length: number): TypedArraySchema => new TypedArraySchema(length, Int32Array); -// @__NO_SIDE_EFFECTS__ +/*#__NO_SIDE_EFFECTS__*/ export const f32Array = (length: number): TypedArraySchema => new TypedArraySchema(length, Float32Array); -// @__NO_SIDE_EFFECTS__ +/*#__NO_SIDE_EFFECTS__*/ export const f64Array = (length: number): TypedArraySchema => new TypedArraySchema(length, Float64Array); diff --git a/packages/typed-binary/src/structure/types.ts b/packages/typed-binary/src/structure/types.ts index 6628f17..be515dd 100644 --- a/packages/typed-binary/src/structure/types.ts +++ b/packages/typed-binary/src/structure/types.ts @@ -1,141 +1,38 @@ import type { IMeasurer, ISerialInput, ISerialOutput } from '../io/types.ts'; -import type { Parsed } from '../utilityTypes.ts'; export type MaxValue = typeof MaxValue; export const MaxValue = Symbol( 'The biggest (in amount of bytes needed) value a schema can represent', ); -export interface IKeyedSchema extends ISchema { - readonly __keyDefinition: TKeyDef; -} - -export type AnyKeyedSchema = IKeyedSchema; +// export interface ISchemaWithProperties> extends ISchema< +// UnwrapRecord +// > { +// readonly properties: TProps; +// } -/** - * Removes one layer of schema wrapping. - * - * @example ``` - * Unwrap>> -> ISchema - * Unwrap> -> number - * ``` - * - * Keyed schemas are bypassed. - * - * @example ``` - * Unwrap>> -> IKeyedSchema<'abc', number> - * ``` - */ -export type Unwrap = - T extends IKeyedSchema - ? // bypassing keyed schemas, as that information has to be preserved for parsing - IKeyedSchema> - : T extends ISchema - ? TInner - : T; - -/** - * Removes one layer of schema wrapping of record properties. - * - * @example ``` - * Unwrap<{ - * a: ISchema, - * b: ISchema> - * }> - * // <=> - * { - * a: number, - * b: ISchema - * } - * ``` - */ -export type UnwrapRecord = - T extends IKeyedSchema> - ? IKeyedSchema }> - : T extends Record - ? { [key in K]: Unwrap } - : T; - -/* helper type for UnwrapArray */ -type __UnwrapArray = T extends unknown[] - ? { - [key in keyof T]: Unwrap; - } - : never; - -/** - * Removes one layer of schema wrapping of array elements. - * - * @example ``` - * Unwrap<[a: ISchema, b: ISchema>]> - * // <=> - * [a: number, b: ISchema] - * ``` - */ -export type UnwrapArray = - T extends IKeyedSchema - ? IKeyedSchema> - : T extends unknown[] - ? __UnwrapArray - : T; - -export interface ISchemaWithProperties> extends ISchema< - UnwrapRecord -> { - readonly properties: TProps; -} +// export type AnySchemaWithProperties = ISchemaWithProperties>; -export type AnySchemaWithProperties = ISchemaWithProperties>; - -export type PropertiesOf = T['properties']; +// export type PropertiesOf = T['properties']; export type PropertyDescription = { bufferOffset: number; - schema: ISchema; + schema: Schema; }; /** - * @param TUnwrap one level of unwrapping to the inferred type. + * @param TIn the JavaScript type accepted by the schema for parsing. + * @param TOut the JavaScript type produced by the schema. */ -export interface ISchema { - readonly __unwrapped: TUnwrapped; - - resolveReferences(ctx: IRefResolver): void; - write(output: ISerialOutput, value: Parsed): void; - read(input: ISerialInput): Parsed; - measure(value: Parsed | MaxValue, measurer?: IMeasurer): IMeasurer; - seekProperty( - reference: Parsed | MaxValue, - prop: keyof TUnwrapped, - ): PropertyDescription | null; -} - -export type AnySchema = ISchema; - -export abstract class Schema implements ISchema { - readonly __unwrapped!: TUnwrapped; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - resolveReferences(ctx: IRefResolver): void { - // override this if you need to resolve internal references. - } - abstract write(output: ISerialOutput, value: Parsed): void; - abstract read(input: ISerialInput): Parsed; - abstract measure(value: Parsed | MaxValue, measurer?: IMeasurer): IMeasurer; - seekProperty( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _reference: Parsed | MaxValue, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _prop: keyof TUnwrapped, - ): PropertyDescription | null { - // override this if necessary. - return null; - } +export interface Schema { + write(output: ISerialOutput, value: TIn): void; + read(input: ISerialInput): TOut; + measure(value: TIn | MaxValue, measurer?: IMeasurer): IMeasurer; } -export class Ref { - constructor(public readonly key: K) {} -} +export type WithSchema = + | Schema + | { readonly ['~typed-binary']: { props: Schema } }; //// // Generic types @@ -147,13 +44,9 @@ export const SubTypeKey = { ENUM: 'enum', } as const; -export interface IRefResolver { - hasKey(key: string): boolean; - - resolve(schemaOrRef: TSchema): TSchema; - register(key: K, schema: ISchema): void; -} - -//// -// Alias types -//// +export type ExtractIn = T extends WithSchema ? TIn : never; +export type ExtractOut = T extends WithSchema ? TOut : never; +export type ExtractInRecord = + T extends Record ? { [K in keyof T]: ExtractIn } : never; +export type ExtractOutRecord = + T extends Record ? { [K in keyof T]: ExtractOut } : never; diff --git a/packages/typed-binary/src/test/bool.test.ts b/packages/typed-binary/src/test/bool.test.ts index 046cb05..64319d9 100644 --- a/packages/typed-binary/src/test/bool.test.ts +++ b/packages/typed-binary/src/test/bool.test.ts @@ -1,14 +1,11 @@ import { describe, expect, it } from 'vitest'; - -// Importing from the public API -import { bool } from '../index.ts'; -// Helpers +import bin from 'typed-binary'; import { encodeAndDecode } from './helpers/mock.ts'; -describe('BoolSchema', () => { +describe('bin.bool', () => { it('should encode and decode a bool value', () => { const value = Math.random() < 0.5; - const decoded = encodeAndDecode(bool, value); + const decoded = encodeAndDecode(bin.bool, value); expect(decoded).to.equal(value); }); diff --git a/packages/typed-binary/src/test/chars.test.ts b/packages/typed-binary/src/test/chars.test.ts index 479a113..0d7cdbe 100644 --- a/packages/typed-binary/src/test/chars.test.ts +++ b/packages/typed-binary/src/test/chars.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'vitest'; -// Importing from the public API -import { CharsSchema } from '../index.ts'; -// Helpers +import bin from 'typed-binary'; import { encodeAndDecode } from './helpers/mock.ts'; import { randIntBetween } from './random.ts'; @@ -20,7 +18,7 @@ describe('CharsSchema', () => { value += String.fromCharCode(randIntBetween(range[0].charCodeAt(0), range[1].charCodeAt(0))); } - const description = new CharsSchema(value.length); + const description = bin.chars(value.length); expect(encodeAndDecode(description, value)).to.equal(value); }); }); diff --git a/packages/typed-binary/src/test/dynamicArray.test.ts b/packages/typed-binary/src/test/dynamicArray.test.ts index 58b3a7a..1925fe5 100644 --- a/packages/typed-binary/src/test/dynamicArray.test.ts +++ b/packages/typed-binary/src/test/dynamicArray.test.ts @@ -1,14 +1,12 @@ import { describe, expect, it } from 'vitest'; -// Importing from the public API -import bin, { DynamicArraySchema, MaxValue } from '../index.ts'; -// Helpers +import bin from 'typed-binary'; import { makeIO } from './helpers/mock.ts'; import { randIntBetween } from './random.ts'; -describe('DynamicArraySchema', () => { +describe('bin.dynamicArray', () => { it('should estimate an int-array encoding size', () => { - const IntArray = bin.dynamicArrayOf(bin.i32); + const IntArray = bin.dynamicArray(bin.i32); const length = randIntBetween(0, 200); const values = []; @@ -16,15 +14,15 @@ describe('DynamicArraySchema', () => { values.push(randIntBetween(-10000, 10000)); } - expect(IntArray.measure(values).size).to.equal( - bin.i32.measure(MaxValue).size + length * bin.i32.measure(MaxValue).size, + expect(IntArray.measure(values).size).toEqual( + bin.i32.measure(bin.MaxValue).size + length * bin.i32.measure(bin.MaxValue).size, ); }); it('should fail to estimate size of max value', () => { - const IntArray = bin.dynamicArrayOf(bin.i32); + const IntArray = bin.dynamicArray(bin.i32); - expect(IntArray.measure(MaxValue).isUnbounded).to.be.true; + expect(IntArray.measure(bin.MaxValue).isUnbounded).toEqual(true); }); it('should encode and decode a simple int array', () => { @@ -34,7 +32,7 @@ describe('DynamicArraySchema', () => { value.push(randIntBetween(-10000, 10000)); } - const description = new DynamicArraySchema(bin.i32); + const description = bin.dynamicArray(bin.i32); const { output, input } = makeIO(length * 4 + 4); // Extra 4 bytes for the length of the array description.write(output, value); diff --git a/packages/typed-binary/src/test/parsed.test.ts b/packages/typed-binary/src/test/extract.test.ts similarity index 51% rename from packages/typed-binary/src/test/parsed.test.ts rename to packages/typed-binary/src/test/extract.test.ts index d337d13..b97cc9e 100644 --- a/packages/typed-binary/src/test/parsed.test.ts +++ b/packages/typed-binary/src/test/extract.test.ts @@ -1,38 +1,47 @@ import { expectTypeOf, it } from 'vitest'; -// Importing from the public API -import bin from '../index.ts'; +import bin from 'typed-binary'; it('parses `i32` properly', () => { - expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); }); it('parses `string` properly', () => { - expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); }); it('parses `optional(string)` properly', () => { const Schema = bin.optional(bin.string); - expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); }); -it('parses `dynamicArrayOf(i32)` properly', () => { - const Schema = bin.dynamicArrayOf(bin.i32); - expectTypeOf>().toEqualTypeOf(); +it('parses `dynamicArray(i32)` properly', () => { + const Schema = bin.dynamicArray(bin.i32); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); }); -it('parses `dynamicArrayOf(string)` properly', () => { - const Schema = bin.dynamicArrayOf(bin.string); - expectTypeOf>().toEqualTypeOf(); +it('parses `dynamicArray(string)` properly', () => { + const Schema = bin.dynamicArray(bin.string); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); }); -it('parses `object({ a: i32, b: string, c: bool })` properly', () => { - const Schema = bin.object({ +it('parses `struct({ a: i32, b: string, c: bool })` properly', () => { + const Schema = bin.struct({ a: bin.i32, b: bin.string, c: bin.bool, }); - expectTypeOf>().toEqualTypeOf<{ + expectTypeOf>().toEqualTypeOf<{ + readonly a: number; + readonly b: string; + readonly c: boolean; + }>(); + expectTypeOf>().toEqualTypeOf<{ a: number; b: string; c: boolean; @@ -48,17 +57,17 @@ it('parses `genericEnum` properly', () => { const Expression = bin.genericEnum( {}, { - [ExpressionType.ADD]: bin.object({ + [ExpressionType.ADD]: { leftHandSizeId: bin.i32, rightHandSizeId: bin.i32, - }), - [ExpressionType.NEGATE]: bin.object({ + }, + [ExpressionType.NEGATE]: { innerExpressionId: bin.i32, - }), + }, }, ); - type Result = bin.Parsed; + type Result = bin.ExtractOut; expectTypeOf().toEqualTypeOf< | { @@ -76,24 +85,24 @@ it('parses `genericEnum` properly', () => { it('parses `generic` with base properties properly', () => { const KeyframeNodeTemplate = bin.generic( { - connections: bin.dynamicArrayOf(bin.i32), + connections: bin.dynamicArray(bin.i32), }, { - 'core:standard': bin.object({ + 'core:standard': { animationKey: bin.string, startFrame: bin.i32, playbackSpeed: bin.i32, looping: bin.bool, - }), - 'core:movement': bin.object({ + }, + 'core:movement': { animationKey: bin.string, startFrame: bin.i32, playbackSpeed: bin.i32, - }), + }, }, ); - type KeyframeNodeTemplate = bin.Parsed; + type KeyframeNodeTemplate = bin.ExtractOut; expectTypeOf().toEqualTypeOf< | { type: 'core:standard'; @@ -114,26 +123,26 @@ it('parses `generic` with base properties properly', () => { }); it('parses simple recursive record properly', () => { - const InfiniteLink = bin.keyed('infinite-link', (InfiniteLink) => - bin.object({ - value: bin.i32, - next: InfiniteLink, - }), - ); + const InfiniteLink = bin.struct({ + value: bin.i32, + get next() { + return InfiniteLink; + }, + }); - expectTypeOf['next']>().toEqualTypeOf< - bin.Parsed + expectTypeOf['next']>().toEqualTypeOf< + bin.ExtractOut >(); }); it('parses tuple schema properly', () => { - const Schema = bin.tupleOf([bin.i32, bin.bool]); + const Schema = bin.tuple([bin.i32, bin.bool]); - expectTypeOf>().toEqualTypeOf<[number, boolean]>(); + expectTypeOf>().toEqualTypeOf<[number, boolean]>(); }); it('parses complex schema properly', () => { - const vec3f = bin.tupleOf([bin.f32, bin.f32, bin.f32]); + const vec3f = bin.tuple([bin.f32, bin.f32, bin.f32]); enum NodeType { // primitives @@ -145,41 +154,41 @@ it('parses complex schema properly', () => { UNION = 3, } - const Sphere = bin.object({ + const Sphere = bin.struct({ pos: vec3f, radius: bin.f32, }); - const Box3 = bin.object({ + const Box3 = bin.struct({ pos: vec3f, halfSize: vec3f, }); - const Plane = bin.object({ + const Plane = bin.struct({ pos: vec3f, normal: vec3f, }); - const Union = bin.object({ + const Union = bin.struct({ smoothRadius: bin.f32, }); - const SceneGraphNode = bin.keyed('node', (SceneGraphNode) => - bin.genericEnum( - { - children: bin.dynamicArrayOf(SceneGraphNode), + const SceneGraphNode = bin.genericEnum( + { + get children() { + return bin.dynamicArray(SceneGraphNode); }, - { - [NodeType.SPHERE]: Sphere, - [NodeType.BOX3]: Box3, - [NodeType.PLANE]: Plane, + }, + { + [NodeType.SPHERE]: Sphere.properties, + [NodeType.BOX3]: Box3.properties, + [NodeType.PLANE]: Plane.properties, - [NodeType.UNION]: Union, - }, - ), + [NodeType.UNION]: Union.properties, + }, ); - type Actual = bin.Parsed; + type Actual = bin.ExtractOut; type Expected = { children: Expected[]; diff --git a/packages/typed-binary/src/test/float.test.ts b/packages/typed-binary/src/test/float.test.ts index f2149a5..e72262d 100644 --- a/packages/typed-binary/src/test/float.test.ts +++ b/packages/typed-binary/src/test/float.test.ts @@ -1,17 +1,15 @@ import { describe, expect, it } from 'vitest'; -// Importing from the public API -import { f16, f32 } from '../index.ts'; -// Helpers +import bin from 'typed-binary'; import { encodeAndDecode } from './helpers/mock.ts'; import { randBetween } from './random.ts'; describe('Float32Schema', () => { it('should encode and decode a f32 value', () => { const value = randBetween(-100, 100); - const decoded = encodeAndDecode(f32, value); + const decoded = encodeAndDecode(bin.f32, value); - expect(decoded).to.closeTo(value, 0.01); + expect(decoded).toBeCloseTo(value, 0.01); }); }); @@ -34,23 +32,23 @@ describe('Float16Schema', () => { // 2^15 × (1 + 1023/1024) - largest representable value const value8 = 65504; - const decoded1 = encodeAndDecode(f16, value1); - const decoded2 = encodeAndDecode(f16, value2); - const decoded3 = encodeAndDecode(f16, value3); - const decoded4 = encodeAndDecode(f16, value4); - const decoded5 = encodeAndDecode(f16, value5); - const decoded6 = encodeAndDecode(f16, value6); - const decoded7 = encodeAndDecode(f16, value7); - const decoded8 = encodeAndDecode(f16, value8); + const decoded1 = encodeAndDecode(bin.f16, value1); + const decoded2 = encodeAndDecode(bin.f16, value2); + const decoded3 = encodeAndDecode(bin.f16, value3); + const decoded4 = encodeAndDecode(bin.f16, value4); + const decoded5 = encodeAndDecode(bin.f16, value5); + const decoded6 = encodeAndDecode(bin.f16, value6); + const decoded7 = encodeAndDecode(bin.f16, value7); + const decoded8 = encodeAndDecode(bin.f16, value8); - expect(decoded1).to.closeTo(value1, 0.0001); - expect(decoded2).to.closeTo(value2, 0.0001); - expect(decoded3).to.closeTo(value3, 0.0001); - expect(decoded4).to.closeTo(value4, 0.0001); - expect(decoded5).to.closeTo(value5, 0.0001); - expect(decoded6).to.closeTo(value6, 0.0001); - expect(decoded7).to.closeTo(value7, 0.0001); - expect(decoded8).to.closeTo(value8, 0.0001); + expect(decoded1).toBeCloseTo(value1, 0.0001); + expect(decoded2).toBeCloseTo(value2, 0.0001); + expect(decoded3).toBeCloseTo(value3, 0.0001); + expect(decoded4).toBeCloseTo(value4, 0.0001); + expect(decoded5).toBeCloseTo(value5, 0.0001); + expect(decoded6).toBeCloseTo(value6, 0.0001); + expect(decoded7).toBeCloseTo(value7, 0.0001); + expect(decoded8).toBeCloseTo(value8, 0.0001); }); it('should encode and decode a f16 value', () => { @@ -59,17 +57,17 @@ describe('Float16Schema', () => { const value3 = 0.34; // precision should be 2^-12 const value4 = 21877.5; // precision should be 16 - const decoded1 = encodeAndDecode(f16, value1); - const decoded2 = encodeAndDecode(f16, value2); - const decoded3 = encodeAndDecode(f16, value3); - const decoded4 = encodeAndDecode(f16, value4); + const decoded1 = encodeAndDecode(bin.f16, value1); + const decoded2 = encodeAndDecode(bin.f16, value2); + const decoded3 = encodeAndDecode(bin.f16, value3); + const decoded4 = encodeAndDecode(bin.f16, value4); // Nearest two representible numbers to 5474 are 5472 and 5476 expect(Math.abs(decoded1 - value1)).toEqual(2); - expect(decoded1).to.closeTo(value1, 4); - expect(decoded2).to.closeTo(value2, 0.25); - expect(decoded3).to.closeTo(value3, 0.000976); - expect(decoded4).to.closeTo(value4, 16); + expect(decoded1).toBeCloseTo(value1, 4); + expect(decoded2).toBeCloseTo(value2, 0.25); + expect(decoded3).toBeCloseTo(value3, 0.000976); + expect(decoded4).toBeCloseTo(value4, 16); }); it('should handle NaN and Infinity', () => { @@ -77,12 +75,12 @@ describe('Float16Schema', () => { const value2 = Number.NEGATIVE_INFINITY; const value3 = Number.NaN; - const decoded1 = encodeAndDecode(f16, value1); - const decoded2 = encodeAndDecode(f16, value2); - const decoded3 = encodeAndDecode(f16, value3); + const decoded1 = encodeAndDecode(bin.f16, value1); + const decoded2 = encodeAndDecode(bin.f16, value2); + const decoded3 = encodeAndDecode(bin.f16, value3); - expect(decoded1).to.equal(value1); - expect(decoded2).to.equal(value2); - expect(decoded3).to.be.NaN; + expect(decoded1).toEqual(value1); + expect(decoded2).toEqual(value2); + expect(decoded3).toBeNaN(); }); }); diff --git a/packages/typed-binary/src/test/helpers/mock.ts b/packages/typed-binary/src/test/helpers/mock.ts index 0fad2da..30e7f41 100644 --- a/packages/typed-binary/src/test/helpers/mock.ts +++ b/packages/typed-binary/src/test/helpers/mock.ts @@ -1,7 +1,8 @@ +import bin from 'typed-binary'; + import { BufferReader } from '../../io/bufferReader.ts'; import { BufferWriter } from '../../io/bufferWriter.ts'; import type { AnySchema } from '../../structure/types.ts'; -import type { Parsed } from '../../utilityTypes.ts'; export function makeIO(bufferSize: number) { const buffer = new ArrayBuffer(bufferSize); @@ -12,10 +13,11 @@ export function makeIO(bufferSize: number) { }; } -export function encodeAndDecode(schema: T, value: Parsed): Parsed { +export function encodeAndDecode( + schema: T, + value: bin.ExtractIn, +): bin.ExtractOut { const buffer = new ArrayBuffer(schema.measure(value).size); - schema.write(new BufferWriter(buffer), value); - - return schema.read(new BufferReader(buffer)) as Parsed; + return schema.read(new BufferReader(buffer)); } diff --git a/packages/typed-binary/src/test/int.test.ts b/packages/typed-binary/src/test/int.test.ts index b7048f9..f9c2717 100644 --- a/packages/typed-binary/src/test/int.test.ts +++ b/packages/typed-binary/src/test/int.test.ts @@ -1,15 +1,13 @@ import { describe, expect, it } from 'vitest'; -// Importing from the public API -import { i8, i16, i32, u8, u16, u32 } from '../index.ts'; -// Helpers +import bin from 'typed-binary'; import { encodeAndDecode } from './helpers/mock.ts'; import { randIntBetween } from './random.ts'; describe('Int8Schema', () => { it('should encode and decode a signed int8 value', () => { const value = randIntBetween(-128, 127); - const decoded = encodeAndDecode(i8, value); + const decoded = encodeAndDecode(bin.i8, value); expect(decoded).to.equal(value); }); @@ -18,7 +16,7 @@ describe('Int8Schema', () => { describe('Uint8Schema', () => { it('should encode and decode a byte value', () => { const value = randIntBetween(0, 256); - const decoded = encodeAndDecode(u8, value); + const decoded = encodeAndDecode(bin.u8, value); expect(decoded).to.equal(value); }); @@ -27,14 +25,14 @@ describe('Uint8Schema', () => { describe('Int16Schema', () => { it('should encode and decode an int value', () => { const value = randIntBetween(-100, 100); - const decoded = encodeAndDecode(i16, value); + const decoded = encodeAndDecode(bin.i16, value); expect(decoded).to.equal(value); }); it('should encode and decode the max pos int value', () => { const value = 2 ** 15 - 1; - const decoded = encodeAndDecode(i16, value); + const decoded = encodeAndDecode(bin.i16, value); expect(decoded).to.equal(value); }); @@ -43,20 +41,20 @@ describe('Int16Schema', () => { describe('Uint16Schema', () => { it('should encode and decode an uint16 value', () => { const value = randIntBetween(0, 100); - const decoded = encodeAndDecode(u16, value); + const decoded = encodeAndDecode(bin.u16, value); expect(decoded).to.equal(value); }); it('should encode and decode the max uint16 value', () => { const value = 2 ** 16 - 1; - const decoded = encodeAndDecode(u16, value); + const decoded = encodeAndDecode(bin.u16, value); expect(decoded).to.equal(value); }); it('max + 1 should overflow into 0', () => { - const decoded = encodeAndDecode(u16, 2 ** 16); + const decoded = encodeAndDecode(bin.u16, 2 ** 16); expect(decoded).to.equal(0); }); @@ -65,14 +63,14 @@ describe('Uint16Schema', () => { describe('Int32Schema', () => { it('should encode and decode an int value', () => { const value = randIntBetween(-100, 100); - const decoded = encodeAndDecode(i32, value); + const decoded = encodeAndDecode(bin.i32, value); expect(decoded).to.equal(value); }); it('should encode and decode the max pos int value', () => { const value = 2 ** 31 - 1; - const decoded = encodeAndDecode(i32, value); + const decoded = encodeAndDecode(bin.i32, value); expect(decoded).to.equal(value); }); @@ -81,20 +79,20 @@ describe('Int32Schema', () => { describe('Uint32Schema', () => { it('should encode and decode an uint value', () => { const value = randIntBetween(0, 100); - const decoded = encodeAndDecode(u32, value); + const decoded = encodeAndDecode(bin.u32, value); expect(decoded).to.equal(value); }); it('should encode and decode the max uint value', () => { const value = 2 ** 32 - 1; - const decoded = encodeAndDecode(u32, value); + const decoded = encodeAndDecode(bin.u32, value); expect(decoded).to.equal(value); }); it('max + 1 should overflow into 0', () => { - const decoded = encodeAndDecode(u32, 2 ** 32); + const decoded = encodeAndDecode(bin.u32, 2 ** 32); expect(decoded).to.equal(0); }); diff --git a/packages/typed-binary/src/test/keyed.test.ts b/packages/typed-binary/src/test/keyed.test.ts deleted file mode 100644 index 119579e..0000000 --- a/packages/typed-binary/src/test/keyed.test.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -// Importing from the public API -import bin from '../index.ts'; -// Helpers -import type { Parsed } from '../utilityTypes.ts'; -import { encodeAndDecode } from './helpers/mock.ts'; - -describe('KeyedSchema', () => { - it('should encode and decode a keyed object, no references', () => { - const Example = bin.keyed('example', () => - bin.object({ - value: bin.i32, - label: bin.string, - }), - ); - - const value = { - value: 70, - label: 'Banana', - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed object, with 0-level-deep references', () => { - const Example = bin.keyed('example', (Example) => - bin.object({ - value: bin.i32, - label: bin.string, - next: bin.optional(Example), - }), - ); - - const value = { - value: 70, - label: 'Banana', - next: undefined, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed object, with 1-level-deep references', () => { - const Example = bin.keyed('example', (Example) => - bin.object({ - value: bin.i32, - label: bin.string, - next: bin.optional(Example), - }), - ); - - const value: Parsed = { - value: 70, - label: 'Banana', - next: { - value: 20, - label: 'Inner Banana', - next: undefined, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed object, with 2-level-deep references', () => { - const Example = bin.keyed('example', (Example) => - bin.object({ - value: bin.i32, - label: bin.string, - next: bin.optional(Example), - }), - ); - - const value: Parsed = { - value: 70, - label: 'Banana', - next: { - value: 20, - label: 'Inner Banana', - next: { - value: 30, - label: 'Level-2 Banana', - next: undefined, - }, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed object, with an inner keyed-object', () => { - type Example = Parsed; - const Example = bin.keyed('example', (Example) => - bin.object({ - label: bin.string, - next: bin.optional(Example), - tree: bin.keyed('tree', (Tree) => - bin.object({ - value: bin.i32, - child: bin.optional(Tree), - }), - ), - }), - ); - - const value: Example = { - label: 'Banana', - next: { - label: 'Inner Banana', - next: undefined, - tree: { - value: 15, - child: undefined, - }, - }, - tree: { - value: 21, - child: { - value: 23, - child: undefined, - }, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed generic object, no references', () => { - type Example = Parsed; - const Example = bin.keyed('example', () => - bin.generic( - { - label: bin.string, - }, - { - primary: bin.object({ - primaryExtra: bin.i32, - }), - secondary: bin.object({ - secondaryExtra: bin.i32, - }), - }, - ), - ); - - const value: Example = { - label: 'Example Label', - type: 'primary', - primaryExtra: 15, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed generic object, with references', () => { - type Example = Parsed; - const Example = bin.keyed('example', (Example) => - bin.generic( - { - label: bin.string, - }, - { - continuous: bin.object({ - next: bin.optional(Example), - }), - fork: bin.object({ - left: bin.optional(Example), - right: bin.optional(Example), - }), - }, - ), - ); - - const value: Example = { - label: 'Root', - type: 'continuous', - next: { - label: 'Level 1', - type: 'fork', - left: { - label: 'Level 2-A', - type: 'continuous', - next: undefined, - }, - right: { - label: 'Level 2-B', - type: 'continuous', - next: undefined, - }, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed enum generic object, no base props, with references', () => { - type Example = Parsed; - const Example = bin.keyed('example', (Example) => - bin.genericEnum( - {}, - { - 0: bin.object({ - next: bin.optional(Example), - }), - 1: bin.object({ - left: bin.optional(Example), - right: bin.optional(Example), - }), - }, - ), - ); - - const value: Example = { - type: 0, - next: { - type: 1, - left: { - type: 0, - next: undefined, - }, - right: { - type: 0, - next: undefined, - }, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed enum generic object, with references', () => { - type Example = Parsed; - const Example = bin.keyed('example', (Example) => - bin.genericEnum( - { - label: bin.string, - }, - { - 0: bin.object({ - next: bin.optional(Example), - }), - 1: bin.object({ - left: bin.optional(Example), - right: bin.optional(Example), - }), - }, - ), - ); - - const value: Example = { - label: 'Root', - type: 0, - next: { - label: 'Level 1', - type: 1, - left: { - label: 'Level 2-A', - type: 0, - next: undefined, - }, - right: { - label: 'Level 2-B', - type: 0, - next: undefined, - }, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); -}); diff --git a/packages/typed-binary/src/test/optional.test.ts b/packages/typed-binary/src/test/optional.test.ts index 2a3d31b..2f22796 100644 --- a/packages/typed-binary/src/test/optional.test.ts +++ b/packages/typed-binary/src/test/optional.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'vitest'; -// Importing from the public API -import bin from '../index.ts'; -// Helpers +import bin from 'typed-binary'; import { makeIO } from './helpers/mock.ts'; import { randIntBetween } from './random.ts'; diff --git a/packages/typed-binary/src/test/object.test.ts b/packages/typed-binary/src/test/struct.test.ts similarity index 66% rename from packages/typed-binary/src/test/object.test.ts rename to packages/typed-binary/src/test/struct.test.ts index 28dd3ec..1efb9f5 100644 --- a/packages/typed-binary/src/test/object.test.ts +++ b/packages/typed-binary/src/test/struct.test.ts @@ -1,23 +1,20 @@ import { describe, expect, expectTypeOf, it } from 'vitest'; -// Importing from the public API -import bin, { type ISchema, MaxValue, type ObjectSchema } from '../index.ts'; -// Helpers -import type { Parsed } from '../utilityTypes.ts'; +import bin from 'typed-binary'; import { encodeAndDecode, makeIO } from './helpers/mock.ts'; -describe('ObjectSchema', () => { +describe('bin.struct', () => { it('should properly estimate size of max value', () => { - const description = bin.object({ + const description = bin.struct({ value: bin.i32, - label: bin.byte, + label: bin.u8, }); - expect(description.measure(MaxValue).size).to.equal(5); + expect(description.measure(bin.MaxValue).size).toEqual(5); }); it('should encode and decode a simple object', () => { - const description = bin.object({ + const description = bin.struct({ value: bin.i32, label: bin.string, extra: bin.u32, @@ -36,7 +33,7 @@ describe('ObjectSchema', () => { it('should treat optional properties as undefined', () => { const OptionalString = bin.optional(bin.string); - const schema = bin.object({ + const schema = bin.struct({ required: bin.string, optional: OptionalString, }); @@ -50,18 +47,18 @@ describe('ObjectSchema', () => { }); it('should encode and decode a generic object', () => { - type GenericType = Parsed; + type GenericType = bin.ExtractOut; const GenericType = bin.generic( { sharedValue: bin.i32, }, { - concrete: bin.object({ + concrete: { extraValue: bin.i32, - }), - other: bin.object({ + }, + other: { notImportant: bin.i32, - }), + }, }, ); @@ -79,18 +76,18 @@ describe('ObjectSchema', () => { }); it('should encode and decode an enum generic object', () => { - type GenericType = Parsed; + type GenericType = bin.ExtractOut; const GenericType = bin.genericEnum( { sharedValue: bin.i32, }, { - 0: bin.object({ + 0: { extraValue: bin.i32, - }), - 1: bin.object({ + }, + 1: { notImportant: bin.i32, - }), + }, }, ); @@ -108,14 +105,14 @@ describe('ObjectSchema', () => { }); it('preserves insertion-order of properties', () => { - const schema = bin.object({ + const schema = bin.struct({ a: bin.i32, c: bin.i32, b: bin.i32, }); // Purposefully out-of-order. - const value: Parsed = { + const value: bin.ExtractOut = { a: 1, b: 2, c: 3, @@ -130,20 +127,20 @@ describe('ObjectSchema', () => { }); it('allows to extend it with more properties', () => { - const schema = bin.object({ + const schema = bin.struct({ a: bin.i32, b: bin.i32, }); const extended = bin.concat([ schema, - bin.object({ + bin.struct({ c: bin.i32, d: bin.i32, }), ]); - const value: Parsed = { + const value: bin.ExtractOut = { a: 1, b: 2, c: 3, @@ -160,20 +157,20 @@ describe('ObjectSchema', () => { }); it('allows to prepend it with more properties', () => { - const schema = bin.object({ + const schema = bin.struct({ a: bin.i32, b: bin.i32, }); const prepended = bin.concat([ - bin.object({ + bin.struct({ c: bin.i32, d: bin.i32, }), schema, ]); - const value: Parsed = { + const value: bin.ExtractOut = { a: 1, b: 2, c: 3, @@ -189,17 +186,50 @@ describe('ObjectSchema', () => { expect(input.readInt32()).to.equal(2); // b }); - it('has a type of ISchema with its properties all unwrapped', () => { - type FlatActual = ObjectSchema<{ a: ISchema }>; - type FlatExpected = ISchema<{ a: number }>; + it('is assignable to AnySchema', () => { + const schema = bin.struct({ a: bin.i32 }); - type NestedActual = ObjectSchema<{ - a: ISchema; - b: ObjectSchema<{ c: ISchema }>; - }>; - type NestedExpected = ISchema<{ a: number; b: { c: number } }>; + function acceptsAny(_s: bin.Schema) {} - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); + acceptsAny(schema); + + expectTypeOf(schema).toEqualTypeOf>(); + expectTypeOf(schema).toExtend(); + }); + + it('is not assignable to a wider or narrower schema', () => { + const schema = bin.struct({ a: bin.i32, b: bin.i32 }); + + function wider(_s: bin.Struct<{ a: bin.Int32 }>) {} + function narrower(_s: bin.Struct<{ a: bin.Int32; b: bin.Int32; c: bin.Int32 }>) {} + + // @ts-expect-error + wider(schema); + + // @ts-expect-error + narrower(schema); + }); + + it('can be recursive', () => { + const Example = bin.struct({ + value: bin.i32, + label: bin.string, + get next() { + return bin.optional(Example); + }, + }); + + const value: bin.ExtractOut = { + value: 70, + label: 'Banana', + next: { + value: 20, + label: 'Inner Banana', + next: undefined, + }, + }; + + const decoded = encodeAndDecode(Example, value); + expect(decoded).to.deep.equal(value); }); }); diff --git a/packages/typed-binary/src/test/tuple.test.ts b/packages/typed-binary/src/test/tuple.test.ts index 787b08c..0b32564 100644 --- a/packages/typed-binary/src/test/tuple.test.ts +++ b/packages/typed-binary/src/test/tuple.test.ts @@ -1,13 +1,11 @@ import { describe, expect, it } from 'vitest'; -// Importing from the public API -import bin from '../index.ts'; -// Helpers +import bin from 'typed-binary'; import { encodeAndDecode } from './helpers/mock.ts'; -describe('TupleSchema', () => { +describe('bin.tuple', () => { it('should estimate an [i32, bool] encoding size', () => { - const Schema = bin.tupleOf([bin.i32, bin.bool]); + const Schema = bin.tuple([bin.i32, bin.bool]); expect(Schema.measure([123, false]).size).toEqual( bin.i32.measure(bin.MaxValue).size + bin.bool.measure(bin.MaxValue).size, @@ -15,7 +13,7 @@ describe('TupleSchema', () => { }); it('should properly estimate size of max value', () => { - const Schema = bin.tupleOf([bin.i32, bin.bool]); + const Schema = bin.tuple([bin.i32, bin.bool]); expect(Schema.measure(bin.MaxValue).size).toEqual( bin.i32.measure(bin.MaxValue).size + bin.bool.measure(bin.MaxValue).size, @@ -23,7 +21,7 @@ describe('TupleSchema', () => { }); it('should encode and decode [i32, bool]', () => { - const Schema = bin.tupleOf([bin.i32, bin.bool]); + const Schema = bin.tuple([bin.i32, bin.bool]); expect(encodeAndDecode(Schema, [123, false])).toEqual([123, false]); }); diff --git a/packages/typed-binary/src/test/typedArray.test.ts b/packages/typed-binary/src/test/typedArray.test.ts index 1792ead..bd9ccd5 100644 --- a/packages/typed-binary/src/test/typedArray.test.ts +++ b/packages/typed-binary/src/test/typedArray.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'vitest'; -// Importing from the public API -import bin from '../index.ts'; -// Helpers +import bin from 'typed-binary'; import { encodeAndDecode } from './helpers/mock.ts'; describe('u8Array', () => { diff --git a/packages/typed-binary/src/test/unwrap.test.ts b/packages/typed-binary/src/test/unwrap.test.ts deleted file mode 100644 index 3725859..0000000 --- a/packages/typed-binary/src/test/unwrap.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { describe, expectTypeOf, it } from 'vitest'; - -// Importing from the public API -import type { IKeyedSchema, ISchema, Unwrap, UnwrapArray, UnwrapRecord } from '../index.ts'; - -describe('Unwrap', () => { - it('unwraps one level of wrapping properly', () => { - type Actual = Unwrap>>; - type Expected = ISchema; - - expectTypeOf().toEqualTypeOf(); - }); - - it('unwraps ONLY one level of wrapping', () => { - type Actual = Unwrap>>>; - type Expected = ISchema>; - - expectTypeOf().toEqualTypeOf(); - }); - - it('bypasses keyed schemas', () => { - type Actual = Unwrap>>>; - type Expected = IKeyedSchema<'abc', ISchema>; - - expectTypeOf().toEqualTypeOf(); - }); - - it('ignores if topmost wrapper (ignoring keyed schemas) is not a schema (record)', () => { - type Inner = { abc: ISchema> }; - type Actual = Unwrap; - - expectTypeOf().toEqualTypeOf(); - }); - - it('ignores if topmost wrapper (ignoring keyed schemas) is not a schema (tuple)', () => { - type Inner = [ISchema, ISchema]; - type Actual = Unwrap; - - expectTypeOf().toEqualTypeOf(); - }); -}); - -describe('UnwrapRecord', () => { - it('unwraps one level of wrapping of direct properties', () => { - type Actual = UnwrapRecord<{ - a: ISchema>; - b: ISchema; - }>; - type Expected = { a: ISchema; b: number }; - - expectTypeOf().toEqualTypeOf(); - }); - - it('bypasses keyed schema at the top level', () => { - type Actual = UnwrapRecord< - IKeyedSchema< - 'xyz', - { - a: ISchema>; - b: ISchema; - } - > - >; - type Expected = IKeyedSchema<'xyz', { a: ISchema; b: number }>; - - expectTypeOf().toEqualTypeOf(); - }); - - it('bypasses keyed schema at the property level', () => { - type Actual = UnwrapRecord<{ - a: IKeyedSchema<'xyz', ISchema>>; - b: IKeyedSchema<'abc', ISchema>; - }>; - type Expected = { - a: IKeyedSchema<'xyz', ISchema>; - b: IKeyedSchema<'abc', number>; - }; - - expectTypeOf().toEqualTypeOf(); - }); - - it('bypasses keyed schema at both levels', () => { - type Actual = IKeyedSchema< - 'top', - UnwrapRecord<{ - a: IKeyedSchema<'xyz', ISchema>>; - b: IKeyedSchema<'abc', ISchema>; - }> - >; - type Expected = IKeyedSchema< - 'top', - { - a: IKeyedSchema<'xyz', ISchema>; - b: IKeyedSchema<'abc', number>; - } - >; - - expectTypeOf().toEqualTypeOf(); - }); -}); - -describe('UnwrapArray', () => { - it('unwraps one level of wrapping of ISchema[]', () => { - type Actual = UnwrapArray[]>; - type Expected = string[]; - - expectTypeOf().toEqualTypeOf(); - }); - - it('unwraps one level of wrapping of [ISchema, number>', () => { - type Actual = UnwrapArray<[ISchema, number]>; - type Expected = [string, number]; - - expectTypeOf().toEqualTypeOf(); - }); - - it('bypasses keyed schema at the top level (simple array)', () => { - type Actual = UnwrapArray[]>>; - type Expected = IKeyedSchema<'xyz', number[]>; - - expectTypeOf().toMatchTypeOf(); - }); - - it('bypasses keyed schema at the top level (tuple)', () => { - type Actual = UnwrapArray>, ISchema]>>; - type Expected = IKeyedSchema<'xyz', [ISchema, string]>; - - expectTypeOf().toMatchTypeOf(); - }); -}); diff --git a/packages/typed-binary/src/utilityTypes.ts b/packages/typed-binary/src/utilityTypes.ts index 75fbc42..0eb058c 100644 --- a/packages/typed-binary/src/utilityTypes.ts +++ b/packages/typed-binary/src/utilityTypes.ts @@ -1,5 +1,3 @@ -import type { IKeyedSchema, ISchema, Ref, Unwrap, UnwrapRecord } from './structure/types.ts'; - /** * @example ``` * type ObjectUnion = ({ a: number, b: number } | { a: number, c: number }); @@ -32,28 +30,10 @@ export type MergeRecordUnion = { [K in DistributedKeyOf]: Extract[K]; }; -export type Parsed< - T, - /** type key dictionary, gets populated during recursive parsing */ - TKeyDict extends { [key in keyof TKeyDict]: TKeyDict[key] } = Record, -> = - T extends IKeyedSchema - ? // A schema that defines themselves under a key in the dictionary - Parsed }> - : T extends ISchema - ? // A non-keyed schema - Parsed - : // A reference to a keyed schema - T extends Ref - ? K extends keyof TKeyDict - ? Parsed - : never - : T extends Record - ? { [K in keyof T]: Parsed } - : T extends unknown[] - ? { [K in keyof T]: Parsed } - : T; - -export type ParseUnwrapped = Parsed>; +export type MutableRecord> = { + -readonly [K in keyof T]: T[K]; +}; -export type ParseUnwrappedRecord = Parsed>; +export type Prettify = { + [K in keyof T]: T[K]; +} & {};