diff --git a/src/structure/array.ts b/src/structure/array.ts index 0479312..da75db2 100644 --- a/src/structure/array.ts +++ b/src/structure/array.ts @@ -1,6 +1,12 @@ import type { ISerialInput, ISerialOutput } from '../io'; import { i32 } from './baseTypes'; -import { IRefResolver, ISchema, IStableSchema, Schema } from './types'; +import { + IRefResolver, + ISchema, + IStableSchema, + Schema, + MaxValue, +} from './types'; export class ArraySchema extends Schema { public elementType: IStableSchema; @@ -37,7 +43,12 @@ export class ArraySchema extends Schema { return array; } - sizeOf(values: T[]): number { + sizeOf(values: T[] | typeof MaxValue): number { + if (values === MaxValue) { + // arrays cannot be bound + return NaN; + } + // Length encoding let size = i32.sizeOf(); // Values encoding diff --git a/src/structure/baseTypes.ts b/src/structure/baseTypes.ts index 47d3933..c06cd60 100644 --- a/src/structure/baseTypes.ts +++ b/src/structure/baseTypes.ts @@ -1,5 +1,5 @@ import { ISerialInput, ISerialOutput } from '../io'; -import { Schema } from './types'; +import { Schema, MaxValue } from './types'; //// // BOOL @@ -42,7 +42,11 @@ export class StringSchema extends Schema { output.writeString(value); } - sizeOf(value: T): number { + sizeOf(value: T | typeof MaxValue): number { + if (value === MaxValue) { + // A string cannot be bound + return NaN; + } return value.length + 1; } } diff --git a/src/structure/index.ts b/src/structure/index.ts index 906cb0f..82f8550 100644 --- a/src/structure/index.ts +++ b/src/structure/index.ts @@ -5,6 +5,7 @@ export { u32, f32, string, + MaxValue, Ref, IRefResolver, Schema, diff --git a/src/structure/keyed.ts b/src/structure/keyed.ts index 548e033..76328d6 100644 --- a/src/structure/keyed.ts +++ b/src/structure/keyed.ts @@ -1,92 +1,113 @@ import { Parsed } from '..'; import { TypedBinaryError } from '../error'; import { ISerialInput, ISerialOutput } from '../io'; -import { IRefResolver, ISchema, IStableSchema, Keyed, Ref } from './types'; +import { + IRefResolver, + ISchema, + IStableSchema, + Keyed, + Ref, + MaxValue, +} from './types'; class RefSchema implements IStableSchema> { - public readonly _infered!: Ref; - public readonly ref: Ref; - - constructor(key: K) { - this.ref = new Ref(key); - } - - resolve(): void { - throw new TypedBinaryError(`Tried to resolve a reference directly. Do it through a RefResolver instead.`); - } - - read(): Ref { - throw new TypedBinaryError(`Tried to read a reference directly. Resolve it instead.`); - } - - write(): void { - throw new TypedBinaryError(`Tried to write a reference directly. Resolve it instead.`); - } - - sizeOf(): number { - throw new TypedBinaryError(`Tried to estimate size of a reference directly. Resolve it instead.`); - } + public readonly _infered!: Ref; + public readonly ref: Ref; + + constructor(key: K) { + this.ref = new Ref(key); + } + + resolve(): void { + throw new TypedBinaryError( + `Tried to resolve a reference directly. Do it through a RefResolver instead.`, + ); + } + + read(): Ref { + throw new TypedBinaryError( + `Tried to read a reference directly. Resolve it instead.`, + ); + } + + write(): void { + throw new TypedBinaryError( + `Tried to write a reference directly. Resolve it instead.`, + ); + } + + sizeOf(): number { + throw new TypedBinaryError( + `Tried to estimate size of a reference directly. Resolve it instead.`, + ); + } } class RefResolve implements IRefResolver { - private registry: {[key: string]: IStableSchema} = {}; - - hasKey(key: string): boolean { - return this.registry[key] !== undefined; + private registry: { [key: string]: IStableSchema } = {}; + + hasKey(key: string): boolean { + return this.registry[key] !== undefined; + } + + register(key: K, schema: IStableSchema): void { + this.registry[key] = schema; + } + + resolve(unstableSchema: ISchema): IStableSchema { + if (unstableSchema instanceof RefSchema) { + const ref = unstableSchema.ref; + const key = ref.key as string; + if (this.registry[key] !== undefined) { + return this.registry[key] as IStableSchema; + } + + throw new TypedBinaryError( + `Couldn't resolve reference to ${key}. Unknown key.`, + ); } - register(key: K, schema: IStableSchema): void { - this.registry[key] = schema; - } - - resolve(unstableSchema: ISchema): IStableSchema { - if (unstableSchema instanceof RefSchema) { - const ref = unstableSchema.ref; - const key = ref.key as string; - if (this.registry[key] !== undefined) { - return this.registry[key] as IStableSchema; - } - - throw new TypedBinaryError(`Couldn't resolve reference to ${key}. Unknown key.`); - } + // Since it's not a RefSchema, we assume it can be resolved. + (unstableSchema as IStableSchema).resolve(this); - // Since it's not a RefSchema, we assume it can be resolved. - (unstableSchema as IStableSchema).resolve(this); - - return unstableSchema as IStableSchema; - } + return unstableSchema as IStableSchema; + } } -export class KeyedSchema> implements ISchema> { - public readonly _infered!: Keyed; - public innerType: S; +export class KeyedSchema> + implements ISchema> +{ + public readonly _infered!: Keyed; + public innerType: S; - constructor(public readonly key: K, innerResolver: (ref: ISchema>) => S) { - this.innerType = innerResolver(new RefSchema(key)); - this._infered = new Keyed(key, this.innerType); + constructor( + public readonly key: K, + innerResolver: (ref: ISchema>) => S, + ) { + this.innerType = innerResolver(new RefSchema(key)); + this._infered = new Keyed(key, this.innerType); - // Automatically resolving after keyed creation. - this.resolve(new RefResolve()); - } + // Automatically resolving after keyed creation. + this.resolve(new RefResolve()); + } - resolve(ctx: IRefResolver): void { - if (!ctx.hasKey(this.key)) { - ctx.register(this.key, this.innerType); + resolve(ctx: IRefResolver): void { + if (!ctx.hasKey(this.key)) { + ctx.register(this.key, this.innerType); - this.innerType.resolve(ctx); - } + this.innerType.resolve(ctx); } + } - read(input: ISerialInput): Parsed { - return this.innerType.read(input) as Parsed; - } + read(input: ISerialInput): Parsed { + return this.innerType.read(input) as Parsed; + } - write(output: ISerialOutput, value: Parsed): void { - this.innerType.write(output, value); - } + write(output: ISerialOutput, value: Parsed): void { + this.innerType.write(output, value); + } - sizeOf(value: Parsed): number { - return this.innerType.sizeOf(value); - } + sizeOf(value: Parsed | typeof MaxValue): number { + return this.innerType.sizeOf(value); + } } - diff --git a/src/structure/object.ts b/src/structure/object.ts index b4d796a..eb215a3 100644 --- a/src/structure/object.ts +++ b/src/structure/object.ts @@ -6,6 +6,7 @@ import { ISchemaWithProperties, SchemaMap, StableSchemaMap, + MaxValue, } from './types'; import { SubTypeKey } from './types'; @@ -73,9 +74,11 @@ export class ObjectSchema return result; } - sizeOf(value: I): number { + sizeOf(value: I | typeof MaxValue): number { return exactEntries(this.properties) - .map(([key, property]) => property.sizeOf(value[key])) // Mapping properties into their sizes. + .map(([key, property]) => + property.sizeOf(value == MaxValue ? MaxValue : value[key]), + ) // Mapping properties into their sizes. .reduce((a, b) => a + b, 0); // Summing them up } } diff --git a/src/structure/optional.ts b/src/structure/optional.ts index 5895c2c..64d9a43 100644 --- a/src/structure/optional.ts +++ b/src/structure/optional.ts @@ -1,45 +1,49 @@ import type { ISerialInput, ISerialOutput } from '../io'; -import { IRefResolver, ISchema, IStableSchema, Schema } from './types'; +import { + IRefResolver, + ISchema, + IStableSchema, + MaxValue, + Schema, +} from './types'; -export class OptionalSchema extends Schema { - private innerSchema: IStableSchema; +export class OptionalSchema extends Schema { + private innerSchema: IStableSchema; - constructor(private readonly _innerUnstableSchema: ISchema) { - super(); + constructor(private readonly _innerUnstableSchema: ISchema) { + super(); - // In case this optional isn't part of a keyed chain, - // let's assume the inner type is stable. - this.innerSchema = _innerUnstableSchema as IStableSchema; - } + // In case this optional isn't part of a keyed chain, + // let's assume the inner type is stable. + this.innerSchema = _innerUnstableSchema as IStableSchema; + } - resolve(ctx: IRefResolver): void { - this.innerSchema = ctx.resolve(this._innerUnstableSchema); - } + resolve(ctx: IRefResolver): void { + this.innerSchema = ctx.resolve(this._innerUnstableSchema); + } - write(output: ISerialOutput, value: T|undefined): void { - if (value !== undefined && value !== null) { - output.writeBool(true); - this.innerSchema.write(output, value); - } - else { - output.writeBool(false); - } + write(output: ISerialOutput, value: T | undefined): void { + if (value !== undefined && value !== null) { + output.writeBool(true); + this.innerSchema.write(output, value); + } else { + output.writeBool(false); } + } - read(input: ISerialInput): T|undefined { - const valueExists = input.readBool(); - - if (valueExists) { - return this.innerSchema.read(input); - } - - return undefined; - } + read(input: ISerialInput): T | undefined { + const valueExists = input.readBool(); - sizeOf(value: T|undefined): number { - if (value === undefined) - return 1; - - return 1 + this.innerSchema.sizeOf(value); + if (valueExists) { + return this.innerSchema.read(input); } + + return undefined; + } + + sizeOf(value: T | undefined | typeof MaxValue): number { + if (value === undefined) return 1; + + return 1 + this.innerSchema.sizeOf(value); + } } diff --git a/src/structure/tuple.ts b/src/structure/tuple.ts index c1049e1..b52e1a8 100644 --- a/src/structure/tuple.ts +++ b/src/structure/tuple.ts @@ -1,43 +1,60 @@ import { TypedBinaryError } from '../error'; -import { IRefResolver, ISchema, IStableSchema, Schema } from './types'; +import { + IRefResolver, + ISchema, + IStableSchema, + MaxValue, + Schema, +} from './types'; import type { ISerialInput, ISerialOutput } from '../io'; export class TupleSchema extends Schema { - private elementSchema: IStableSchema; - - constructor(private readonly _unstableElementSchema: ISchema, public readonly length: number) { - super(); - - // In case this array isn't part of a keyed chain, - // let's assume the inner type is stable. - this.elementSchema = _unstableElementSchema as IStableSchema; + private elementSchema: IStableSchema; + + constructor( + private readonly _unstableElementSchema: ISchema, + public readonly length: number, + ) { + super(); + + // In case this array isn't part of a keyed chain, + // let's assume the inner type is stable. + this.elementSchema = _unstableElementSchema as IStableSchema; + } + + resolve(ctx: IRefResolver): void { + this.elementSchema = ctx.resolve(this._unstableElementSchema); + } + + write(output: ISerialOutput, values: T[]): void { + if (values.length !== this.length) { + throw new TypedBinaryError( + `Expected tuple of length ${this.length}, got ${values.length}`, + ); } - resolve(ctx: IRefResolver): void { - this.elementSchema = ctx.resolve(this._unstableElementSchema); + for (const value of values) { + this.elementSchema.write(output, value); } + } - write(output: ISerialOutput, values: T[]): void { - if (values.length !== this.length) { - throw new TypedBinaryError(`Expected tuple of length ${this.length}, got ${values.length}`); - } - - for (const value of values) { - this.elementSchema.write(output, value); - } + read(input: ISerialInput): T[] { + const array = []; + + for (let i = 0; i < this.length; ++i) { + array.push(this.elementSchema.read(input)); } - read(input: ISerialInput): T[] { - const array = []; + return array; + } - for (let i = 0; i < this.length; ++i) { - array.push(this.elementSchema.read(input)); - } - - return array; + sizeOf(values: T[] | typeof MaxValue): number { + if (values === MaxValue) { + return this.length * this.elementSchema.sizeOf(MaxValue); } - sizeOf(values: T[]): number { - return values.map(v => this.elementSchema.sizeOf(v)).reduce((a, b) => a + b); - } + return values + .map((v) => this.elementSchema.sizeOf(v)) + .reduce((a, b) => a + b); + } } diff --git a/src/structure/types.ts b/src/structure/types.ts index 9ffdc86..123d743 100644 --- a/src/structure/types.ts +++ b/src/structure/types.ts @@ -1,40 +1,43 @@ import { ISerialInput, ISerialOutput } from '../io'; +export const MaxValue = Symbol('The maximum value a schema can hold'); + /** * A schema that hasn't been resolved yet (a reference). * Distinguishing between a "stable" schema, and an "unstable" schema * helps to avoid errors in usage of unresolved schemas (the lack of utility functions). */ export interface ISchema { - readonly _infered: I; + readonly _infered: I; } -export interface ISchemaWithProperties extends ISchema { - readonly properties: StableSchemaMap; +export interface ISchemaWithProperties + extends ISchema { + readonly properties: StableSchemaMap; } export interface IStableSchema extends ISchema { - resolve(ctx: IRefResolver): void; - write(output: ISerialOutput, value: I): void; - read(input: ISerialInput): I; - sizeOf(value: I): number; + resolve(ctx: IRefResolver): void; + write(output: ISerialOutput, value: I): void; + read(input: ISerialInput): I; + sizeOf(value: I | typeof MaxValue): number; } export abstract class Schema implements IStableSchema { - readonly _infered!: I; + readonly _infered!: I; - abstract resolve(ctx: IRefResolver): void; - abstract write(output: ISerialOutput, value: I): void; - abstract read(input: ISerialInput): I; - abstract sizeOf(value: I): number; + abstract resolve(ctx: IRefResolver): void; + abstract write(output: ISerialOutput, value: I): void; + abstract read(input: ISerialInput): I; + abstract sizeOf(value: I | typeof MaxValue): number; } export class Ref { - constructor(public readonly key: K) {} + constructor(public readonly key: K) {} } export class Keyed> { - constructor(public readonly key: K, public readonly innerType: S) {} + constructor(public readonly key: K, public readonly innerType: S) {} } //// @@ -42,20 +45,20 @@ export class Keyed> { //// export enum SubTypeKey { - STRING = 'STRING', - ENUM = 'ENUM', + STRING = 'STRING', + ENUM = 'ENUM', } export interface IRefResolver { - hasKey(key: string): boolean; + hasKey(key: string): boolean; - resolve(schemaOrRef: ISchema): IStableSchema; - register(key: K, schema: IStableSchema): void; + resolve(schemaOrRef: ISchema): IStableSchema; + register(key: K, schema: IStableSchema): void; } //// // Alias types //// -export type SchemaMap = {[key in keyof T]: ISchema}; -export type StableSchemaMap = {[key in keyof T]: IStableSchema}; \ No newline at end of file +export type SchemaMap = { [key in keyof T]: ISchema }; +export type StableSchemaMap = { [key in keyof T]: IStableSchema }; diff --git a/src/test/array.test.ts b/src/test/array.test.ts index 76b8e1b..4a433f6 100644 --- a/src/test/array.test.ts +++ b/src/test/array.test.ts @@ -3,6 +3,7 @@ import { randIntBetween } from './random'; import { makeIO } from './_mock.test'; import { ArraySchema, i32 } from '../structure'; import { arrayOf } from '..'; +import { MaxValue } from '../structure/types'; const expect = chai.expect; @@ -21,6 +22,12 @@ describe('ArraySchema', () => { ); }); + it('should fail to estimate size of max value', () => { + const IntArray = arrayOf(i32); + + expect(IntArray.sizeOf(MaxValue)).to.be.NaN; + }); + it('should encode and decode a simple int array', () => { const length = randIntBetween(0, 5); const value = [];