Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: recursive ToJSON #174

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { CollectionSchema } from './types/CollectionSchema';
import { SetSchema } from './types/SetSchema';

import { ChangeTree, Ref, ChangeOperation } from "./changes/ChangeTree";
import { NonFunctionPropNames, ToJSON } from './types/HelperTypes';
import { NonFunctionPropNames, NonFunctionProps, ToJSON } from './types/HelperTypes';
import { ClientState } from './filters';
import { getType } from './types/typeRegistry';
import { ReferenceTracker } from './changes/ReferenceTracker';
Expand Down Expand Up @@ -894,7 +894,9 @@ export abstract class Schema {
return cloned;
}

toJSON () {
toJSON (): NonFunctionProps<{
[field in keyof this]: ToJSON<this[field]>
}> {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this type is not entirely accurate, because there is a conditional that seems to omit null values. This hasn't been taken into account, but this isn't being taken into account in the current implementation anyways.

const schema = this._definition.schema;
const deprecated = this._definition.deprecated;

Expand All @@ -906,7 +908,9 @@ export abstract class Schema {
: this[`_${field}`];
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this part: why is there an underscore?

}
}
return obj as ToJSON<typeof this>;
return obj as NonFunctionProps<{
[field in keyof this]: ToJSON<this[field]>
}>
}

discardAllChanges() {
Expand Down
4 changes: 2 additions & 2 deletions src/types/ArraySchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChangeTree } from "../changes/ChangeTree";
import { OPERATION } from "../spec";
import { SchemaDecoderCallbacks, Schema } from "../Schema";
import { addCallback, removeChildRefs } from "./utils";
import { DataChange } from "..";
import { DataChange, ToJSON } from "..";

const DEFAULT_SORT = (a: any, b: any) => {
const A = a.toString();
Expand Down Expand Up @@ -654,7 +654,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
return Array.from(this.$items.values());
}

toJSON() {
toJSON(): ToJSON<V>[] {
return this.toArray().map((value) => {
return (typeof (value['toJSON']) === "function")
? value['toJSON']()
Expand Down
6 changes: 3 additions & 3 deletions src/types/CollectionSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChangeTree } from "../changes/ChangeTree";
import { OPERATION } from "../spec";
import { SchemaDecoderCallbacks } from "../Schema";
import { addCallback, removeChildRefs } from "./utils";
import { DataChange } from "..";
import { DataChange, ToJSON } from "..";

type K = number; // TODO: allow to specify K generic on MapSchema.

Expand Down Expand Up @@ -157,8 +157,8 @@ export class CollectionSchema<V=any> implements SchemaDecoderCallbacks {
return Array.from(this.$items.values());
}

toJSON() {
const values: V[] = [];
toJSON(): ToJSON<V>[] {
const values: ToJSON<V>[] = [];

this.forEach((value, key) => {
values.push(
Expand Down
13 changes: 4 additions & 9 deletions src/types/HelperTypes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ArraySchema } from "./ArraySchema";
import { MapSchema } from "./MapSchema";
import { Schema } from "../Schema";

export type NonFunctionProps<T> = Omit<T, {
[K in keyof T]: T[K] extends Function ? K : never;
Expand All @@ -9,12 +10,6 @@ export type NonFunctionPropNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K
}[keyof T];

export type ToJSON<T> = NonFunctionProps<{
[K in keyof T]: T[K] extends MapSchema<infer U>
? Record<string, U>
: T[K] extends Map<string, infer U>
? Record<string, U>
: T[K] extends ArraySchema<infer U>
? U[]
: T[K]
}>;
export type ToJSON<T> = T extends {
toJSON(): unknown
} ? ReturnType<T['toJSON']> : T
4 changes: 2 additions & 2 deletions src/types/MapSchema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SchemaDecoderCallbacks } from "../Schema";
import { addCallback, removeChildRefs } from "./utils";
import { DataChange } from "..";
import { DataChange, ToJSON } from "..";
import { ChangeTree } from "../changes/ChangeTree";
import { OPERATION } from "../spec";

Expand Down Expand Up @@ -243,7 +243,7 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, S
this.$indexes.delete(index);
}

toJSON() {
toJSON(): Record<K, ToJSON<V>> {
const map: any = {};

this.forEach((value, key) => {
Expand Down
6 changes: 3 additions & 3 deletions src/types/SetSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChangeTree } from "../changes/ChangeTree";
import { OPERATION } from "../spec";
import { SchemaDecoderCallbacks } from "../Schema";
import { addCallback, removeChildRefs } from "./utils";
import { DataChange } from "..";
import { DataChange, ToJSON } from "..";

export class SetSchema<V=any> implements SchemaDecoderCallbacks {
protected $changes: ChangeTree = new ChangeTree(this);
Expand Down Expand Up @@ -166,8 +166,8 @@ export class SetSchema<V=any> implements SchemaDecoderCallbacks {
return Array.from(this.$items.values());
}

toJSON() {
const values: V[] = [];
toJSON(): ToJSON<V>[] {
const values: ToJSON<V>[] = [];

this.forEach((value, key) => {
values.push(
Expand Down
7 changes: 7 additions & 0 deletions test/helpers/Equals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Used to check if two types are equal.
// If they are equal, the expression evaluates to true, otherwise false
export type Equals<T1, T2> = T1 extends T2
? T2 extends T1
? true
: false
: false
173 changes: 173 additions & 0 deletions test/src/HelperTypes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { ArraySchema, CollectionSchema, MapSchema, Schema, SetSchema, ToJSON, type } from "../../src";
import { Equals } from "../helpers/Equals";

// Reused across multiple tests
export class VecSchema extends Schema {
@type('number') x: number
@type('number') y: number
}

describe("ToJSON type tests", () => {
it("Omits methods", () => {
class C extends Schema {
@type('number') time: number
rewind(arg: number){}
}
const _t1: Equals<ToJSON<C>, {
time: number
}> = true
})

it("Does not transform primitive types", () => {
// Primitive types have methods, and these should not be omitted
const _tString: Equals<ToJSON<string>, string> = true
const _tNumber: Equals<ToJSON<number>, number> = true
const _tBoolean: Equals<ToJSON<boolean>, boolean> = true
const _tBigInt: Equals<ToJSON<bigint>, bigint> = true
const _tSymbol: Equals<ToJSON<symbol>, symbol> = true
const _tUndefined: Equals<ToJSON<undefined>, undefined> = true
const _tNull: Equals<ToJSON<null>, null> = true
})

it("Does not transform non-schema types", () => {
class C extends Schema {
time: number
pos: { x: number, y: number }
}
const _t1: Equals<ToJSON<C>, {
time: number
pos: { x: number, y: number }
}> = true
const _t2: ToJSON<C> = {
time: 1,
pos: { x: 1, y: 2 }
}
})

it("Primitive type on root", () => {
class C extends Schema {
@type('number') time: number
@type('string') name: string
}
const _t1: Equals<ToJSON<C>, {
time: number
name: string
}> = true
const _t2: ToJSON<C> = {
time: 1,
name: "name"
}
})

it("Schema type on root", () => {
class C extends Schema {
@type(VecSchema) ballPos: VecSchema
}
const _t1: Equals<ToJSON<C>, {
ballPos: {
x: number
y: number
}
}> = true
const _t2: ToJSON<C> = {
ballPos: {
x: 1,
y: 2
}
}
})

it("Map on root", () => {
class C extends Schema {
@type({map: VecSchema}) positions: MapSchema<VecSchema>
}
const _t1: Equals<ToJSON<C>, {
positions: Record<string, {
x: number
y: number
}>
}> = true
const _t2: ToJSON<C> = {
positions: {
a: {
x: 1,
y: 2
}
}
}
})

it("MapSchema on root", () => {
class C extends Schema {
@type({map: VecSchema}) positions: MapSchema<VecSchema>
}
const _t1: Equals<ToJSON<C>, {
positions: Record<string, {
x: number
y: number
}>
}> = true
const _t2: ToJSON<C> = {
positions: {
a: {
x: 1,
y: 2
}
}
}
})

it("ArraySchema on root", () => {
class C extends Schema {
@type({map: VecSchema}) positions: ArraySchema<VecSchema>
}
const _t1: Equals<ToJSON<C>, {
positions: Array<{
x: number
y: number
}>
}> = true
const _t2: ToJSON<C> = {
positions: [{
x: 1,
y: 2
}]
}
})

it("SetSchema on root", () => {
class C extends Schema {
@type({map: VecSchema}) positions: SetSchema<VecSchema>
}
const _t1: Equals<ToJSON<C>, {
positions: Array<{
x: number
y: number
}>
}> = true
const _t2: ToJSON<C> = {
positions: [{
x: 1,
y: 2
}]
}
})

it("CollectionSchema on root", () => {
class C extends Schema {
@type({map: VecSchema}) positions: CollectionSchema<VecSchema>
}
const _t1: Equals<ToJSON<C>, {
positions: Array<{
x: number
y: number
}>
}> = true
const _t2: ToJSON<C> = {
positions: [{
x: 1,
y: 2
}]
}
})
})
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 41 tests some types, but these types are never checked unless this file is moved into a subdirectory to the test folder.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as assert from "assert";
import { State, Player, DeepState, DeepMap, DeepChild, Position, DeepEntity } from "./Schema";
import { Schema, ArraySchema, MapSchema, type } from "../src";
import { State, Player, DeepState, DeepMap, DeepChild, Position, DeepEntity } from "../Schema";
import { Schema, ArraySchema, MapSchema, type } from "../../src";

describe("TypeScript Types", () => {
it("strict null/undefined checks", () => {
Expand Down