Skip to content

Commit

Permalink
Feature/recursive types (#1)
Browse files Browse the repository at this point in the history
* Object properties can now get lazy processed. (+more)
- Recursive types now work, but don't yet enforce consistency.

* Undefined properties are now optional.

* Refactorization
- Reduced the unnecessary prevelence of OptionalUndefined
- Removed the redundant "_infered" holder property.

* Fixed nested recursive types.

* Fixed the typed recursive tests.

* Updated the README, describing recursive types.

* Fixed the array.test naming.

* 1.1.0
  • Loading branch information
iwoplaza authored Jan 25, 2022
1 parent 6f61079 commit 80881e4
Show file tree
Hide file tree
Showing 15 changed files with 278 additions and 57 deletions.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Gives tools to describe binary structures with full TypeScript support. Encodes
- [Arrays](#arrays)
- [Tuples](#tuples)
- [Optionals](#optionals)
- [Recursive types](#recursive-types)
- [Serialization and Deserialization](#serialization-and-deserialization)

# Features:
Expand Down Expand Up @@ -334,6 +335,60 @@ console.log(JSON.stringify(Person.read(reader).address)); // undefined
console.log(JSON.stringify(Person.read(reader).address)); // { "city": "New York", ... }
```

# Recursive types
If you want an object type to be able to contain one of itself (recursion), then you have to apply the following pattern:
```ts
import { INT, STRING, object, Parsed, ParsedConcrete, typedGeneric, typedObject, TypeToken } from 'typed-binary';

interface ExpressionBase {}

interface MultiplyExpression extends ExpressionBase {
type: 'multiply';
a: Expression;
b: Expression;
}

interface NegateExpression extends ExpressionBase {
type: 'negate';
inner: Expression;
}

type IntLiteralExpression = ParsedConcrete<ExpressionBase, typeof IntLiteralExpression, 'int_literal'>;
const IntLiteralExpression = object({
value: INT,
});

type Expression = MultiplyExpression|NegateExpression|IntLiteralExpression;
const Expression = typedGeneric(new TypeToken<Expression>(), {
name: STRING,
}, {
'multiply': typedObject<MultiplyExpression>(() => ({
a: Expression,
b: Expression,
})),
'negate': typedObject<NegateExpression>(() => ({
inner: Expression,
})),
'int_literal': IntLiteralExpression
});

const expr: Parsed<typeof Expression> = {
type: 'multiply',
a: {
type: 'negate',
inner: {
type: 'int_literal',
value: 15,
}
},
b: {
type: 'int_literal',
value: 2,
},
};

```

# Serialization and Deserialization
Each schema has the following methods:
```ts
Expand Down
3 changes: 2 additions & 1 deletion examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "typed-binary-examples",
"scripts": {
"link": "npm link ../",
"example:binaryMesh": "ts-node binaryMesh/index.ts"
"example:binaryMesh": "ts-node binaryMesh/index.ts",
"example:recursiveTypes": "ts-node recursiveTypes/index.ts"
},
"devDependencies": {
"ts-node": "^10.4.0",
Expand Down
53 changes: 53 additions & 0 deletions examples/recursiveTypes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// Run with `npm run example:recursiveTypes`
//

import { INT, object, Parsed, ParsedConcrete, STRING, typedGeneric, typedObject, TypeToken } from 'typed-binary';

interface ExpressionBase {}

interface MultiplyExpression extends ExpressionBase {
type: 'multiply';
a: Expression;
b: Expression;
}

interface NegateExpression extends ExpressionBase {
type: 'negate';
inner: Expression;
}

type IntLiteralExpression = ParsedConcrete<ExpressionBase, typeof IntLiteralExpression, 'int_literal'>;
const IntLiteralExpression = object({
value: INT,
});

type Expression = MultiplyExpression|NegateExpression|IntLiteralExpression;
const Expression = typedGeneric(new TypeToken<Expression>(), {
name: STRING,
}, {
'multiply': typedObject<MultiplyExpression>(() => ({
a: Expression,
b: Expression,
})),
'negate': typedObject<NegateExpression>(() => ({
inner: Expression,
})),
'int_literal': IntLiteralExpression
});

const expr: Parsed<typeof Expression> = {
type: 'multiply',
a: {
type: 'negate',
inner: {
type: 'int_literal',
value: 15,
}
},
b: {
type: 'int_literal',
value: 2,
},
};

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "typed-binary",
"version": "1.0.0",
"version": "1.1.0",
"description": "Describe binary structures with full TypeScript support. Encode and decode into pure JavaScript objects.",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
Expand Down
27 changes: 20 additions & 7 deletions src/describe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,49 @@ import { ArraySchema } from '../structure/array';
import { OptionalSchema } from '../structure/optional';
import { GenericObjectSchema } from '../structure/object';
import { TupleSchema } from '../structure/tuple';
import { Schema, SchemaProperties } from '../structure/types';
import { ValueOrProvider } from '../utilityTypes';

import { ISchema, SchemaProperties } from '../structure/types';
import { OptionalUndefined, ValueOrProvider } from '../utilityTypes';
import { Parsed } from '..';


export const chars = <T extends number>(length: T) =>
new CharsSchema(length);

export const object = <P extends SchemaProperties>(properties: P) =>
export const object = <P extends SchemaProperties>(properties: ValueOrProvider<P>) =>
new ObjectSchema(properties);

export const typedObject = <P extends {[key in keyof P]: P[key]}>(properties: ValueOrProvider<unknown>) =>
new ObjectSchema<any, OptionalUndefined<P>>(properties);

export const generic = <P extends SchemaProperties, S extends {[key in keyof S]: ObjectSchema<any>}>(properties: P, subTypeMap: ValueOrProvider<S>) =>
new GenericObjectSchema(
SubTypeKey.STRING as any,
properties,
subTypeMap
);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class TypeToken<I> {}

export const typedGeneric = <P extends {[key in keyof P]: P[key]} & { type: string }>(token: TypeToken<P>, properties: ValueOrProvider<unknown>, subTypeMap: any) =>
new GenericObjectSchema<any, any, SubTypeKey.STRING, P>(
SubTypeKey.STRING,
properties,
subTypeMap
);

export const genericEnum = <P extends SchemaProperties, S extends {[key in keyof S]: ObjectSchema<any>}>(properties: P, subTypeMap: ValueOrProvider<S>) =>
new GenericObjectSchema(
SubTypeKey.ENUM as any,
properties,
subTypeMap
);

export const arrayOf = <T extends Schema<T['_infered']>>(elementType: T) =>
export const arrayOf = <T extends ISchema<Parsed<T>>>(elementType: T) =>
new ArraySchema(elementType);

export const tupleOf = <T extends Schema<T['_infered']>>(elementType: T, length: number) =>
export const tupleOf = <T extends ISchema<Parsed<T>>>(elementType: T, length: number) =>
new TupleSchema(elementType, length);

export const optional = <I, T extends Schema<I>>(innerType: T) =>
export const optional = <T extends ISchema<Parsed<T>>>(innerType: T) =>
new OptionalSchema(innerType);
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './structure';
export * from './describe';
export * from './io';
export type { Parsed } from './utilityTypes';
export type { Parsed, ParsedConcrete } from './utilityTypes';
2 changes: 1 addition & 1 deletion src/parsed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ export const KeyframeNodeTemplate =
});

type KeyframeNodeTemplate = Parsed<typeof KeyframeNodeTemplate>;
// const s = {} as KeyframeNodeTemplate;
// const s = {} as KeyframeNodeTemplate;
55 changes: 38 additions & 17 deletions src/structure/object.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { Parsed } from '..';
import type { ISerialInput, ISerialOutput } from '../io';
import type { ValueOrProvider } from '../utilityTypes';
import type { OptionalUndefined, ValueOrProvider } from '../utilityTypes';
import { STRING } from './baseTypes';
import {
Schema, InferedProperties, SchemaProperties
} from './types';
import { SubTypeKey } from './types';


export class ObjectSchema<T extends SchemaProperties, O extends InferedProperties<T> = InferedProperties<T>> extends Schema<O> {
constructor(public readonly properties: T) {
export class ObjectSchema<
T extends SchemaProperties,
O extends OptionalUndefined<InferedProperties<T>> = OptionalUndefined<InferedProperties<T>>
> extends Schema<O> {
private cachedProperties?: T;

constructor(private readonly _properties: ValueOrProvider<T>) {
super();
}

public get properties() {
return this.cachedProperties || (
this.cachedProperties = (typeof this._properties === 'function' ? this._properties() : this._properties)
);
}

write(output: ISerialOutput, value: O): void {
const keys: string[] = Object.keys(this.properties);

for (const key of keys) {
this.properties[key].write(output, value[key]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.properties[key].write(output, (value as any)[key]);
}
}

Expand All @@ -25,8 +38,11 @@ export class ObjectSchema<T extends SchemaProperties, O extends InferedPropertie
const result = {} as O;

for (const key of keys) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
result[key] = this.properties[key].read(input) as O[typeof key];
const value = this.properties[key].read(input);
if (value !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(result as any)[key] = value;
}
}

return result;
Expand All @@ -37,27 +53,29 @@ export class ObjectSchema<T extends SchemaProperties, O extends InferedPropertie

// Going through the base properties
size += Object.keys(this.properties)
.map(key => this.properties[key].sizeOf(value[key])) // Mapping properties into their sizes.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
.map(key => this.properties[key].sizeOf((value as any)[key])) // Mapping properties into their sizes.
.reduce((a, b) => a + b); // Summing them up

return size;
}
}

type InferedSubTypes<T extends {[key in keyof T]: ObjectSchema<SchemaProperties>}> = {
[Key in keyof T]: T[Key]['_infered'] & { type: Key }
export type InferedSubTypes<T extends {[key in keyof T]: ObjectSchema<SchemaProperties>}> = {
[Key in keyof T]: Parsed<T[Key]> & { type: Key }
};

export type ObjectSchemaMap<S, SI extends {[key in keyof SI]: SI[key]}> = {[key in keyof S]: ObjectSchema<SI[key]>};

export class GenericObjectSchema<
T extends SchemaProperties, // Base properties
S extends {[Key in keyof S]: ObjectSchema<SchemaProperties>}, // Sub type map
K extends ((keyof S) extends string ? SubTypeKey.STRING : SubTypeKey.ENUM)
> extends ObjectSchema<T, InferedProperties<T> & InferedSubTypes<S>[keyof S]> {
K extends string|number,
I extends OptionalUndefined<InferedProperties<T>> & InferedSubTypes<S>[keyof S] = OptionalUndefined<InferedProperties<T>> & InferedSubTypes<S>[keyof S]
> extends ObjectSchema<T, I> {
constructor(
public readonly keyedBy: K,
properties: T,
properties: ValueOrProvider<T>,
private readonly subTypeMap: ValueOrProvider<S>
) {
super(properties);
Expand All @@ -67,7 +85,7 @@ export class GenericObjectSchema<
return typeof this.subTypeMap === 'function' ? this.subTypeMap() : this.subTypeMap;
}

write(output: ISerialOutput, value: InferedProperties<T> & InferedSubTypes<S>[keyof S]): void {
write(output: ISerialOutput, value: I): void {
// Figuring out sub-types
const subTypeDescription = this.getSubTypeMap()[value.type] || null;
if (subTypeDescription === null) {
Expand Down Expand Up @@ -97,7 +115,7 @@ export class GenericObjectSchema<
}
}

read(input: ISerialInput): InferedProperties<T> & InferedSubTypes<S>[keyof S] {
read(input: ISerialInput): I {
const subTypeMap = this.getSubTypeMap();
const subTypeKey = this.keyedBy === SubTypeKey.ENUM ? input.readByte() : input.readString();

Expand All @@ -116,15 +134,18 @@ export class GenericObjectSchema<

for (const key of extraKeys) {
const prop = (subTypeDescription.properties)[key];
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(result as any)[key] = prop.read(input);
const value = prop.read(input);
if (value !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(result as any)[key] = value;
}
}
}

return result;
}

sizeOf(value: InferedProperties<T> & InferedSubTypes<S>[keyof S]): number {
sizeOf(value: I): number {
let size = super.sizeOf(value);

// We're a generic object trying to encode a concrete value.
Expand Down
15 changes: 3 additions & 12 deletions src/structure/types.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { Parsed } from '..';
import { ISerialInput, ISerialOutput } from '../io';

export interface ISchema<P> {
/**
* Used as a type holder, so that type inference works correctly.
*/
readonly _infered: P;

write(output: ISerialOutput, value: P): void;
read(input: ISerialInput): P;
sizeOf(value: P): number;
}

export abstract class Schema<P> implements ISchema<P> {
/**
* Used as a type holder, so that type inference works correctly.
*/
readonly _infered!: P;

abstract write(output: ISerialOutput, value: P): void;
abstract read(input: ISerialInput): P;
abstract sizeOf(value: P): number;
Expand All @@ -31,9 +22,9 @@ export enum SubTypeKey {
ENUM = 'ENUM',
}

// export type SchemaProperties<T> = T extends {[key in keyof T]: Schema<any>} ? {[key in keyof T]: Schema<T[key]['_infered']>} : never;
// export type SchemaProperties<T> = T extends {[key in keyof T]: Schema<any>} ? {[key in keyof T]: Schema<Parsed<T[key]>>} : never;
export type SchemaProperties = {[key: string]: Schema<unknown>};
export type InferedProperties<T extends {[key: string]: Schema<unknown>}> = {[key in keyof T]: T[key]['_infered']};
export type InferedProperties<T extends {[key: string]: Schema<unknown>}> = {[key in keyof T]: Parsed<T[key]>};

export interface IConcreteObjectSchema<T extends SchemaProperties> extends ISchema<InferedProperties<T>> {
readonly properties: T;
Expand Down
Loading

0 comments on commit 80881e4

Please sign in to comment.