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

Add support for reviving symbols #46

Merged
merged 27 commits into from
Aug 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7637350
Add ClassRegistry
Skn0tt Jul 31, 2020
acad253
Add test describing what shall be achieved
Skn0tt Jul 31, 2020
a017f2e
Implement class revival
Skn0tt Jul 31, 2020
09c7a16
add test for accessors
Skn0tt Jul 31, 2020
70f2912
Remove type overrides
Skn0tt Jul 31, 2020
3d336a7
Serialize maps using arrays of pairs
Skn0tt Aug 1, 2020
2633613
Add first go on compressing paths into tree
Skn0tt Aug 2, 2020
6182b20
Add treecompressor
Skn0tt Aug 2, 2020
6730f1b
Use treeifier for `value` annotations
Skn0tt Aug 2, 2020
639e025
Also model `referentialIdentites` using the tree
Skn0tt Aug 2, 2020
fc665bd
Create highly-specialised pathtree
Skn0tt Aug 2, 2020
f74393a
add minimizer
Skn0tt Aug 2, 2020
3f0337b
Use specialized pathtree instead
Skn0tt Aug 2, 2020
7e4bd33
Remove unnused function `isStringifiedPath`
Skn0tt Aug 2, 2020
2138b86
Clean up the codebase
Skn0tt Aug 2, 2020
cf4fe4e
Merge branch 'revive-classes' into compress-paths-into-tree
Skn0tt Aug 2, 2020
bfa72ab
Merge branch 'model-maps-using-collection-of-entry-pairs' into compre…
Skn0tt Aug 3, 2020
2943b9a
Merge branch 'main' into compress-paths-into-tree
Skn0tt Aug 3, 2020
8a02b90
Update README to reflect pathtree structure
Skn0tt Aug 3, 2020
291a5f2
Improve performance by not parsing paths too often
Skn0tt Aug 3, 2020
5e15bc5
fix benchmark
Skn0tt Aug 3, 2020
42e1b9f
Extract registry into its own file
Skn0tt Aug 3, 2020
c18089b
Create SymbolRegistry
Skn0tt Aug 3, 2020
167666d
Add support for symbols
Skn0tt Aug 3, 2020
b268edb
Merge branch 'main' into compress-paths-into-tree
Skn0tt Aug 10, 2020
8d84788
Merge branch 'compress-paths-into-tree' into revive-symbols
Skn0tt Aug 10, 2020
681df1c
Merge branch 'main' into revive-symbols
Skn0tt Aug 12, 2020
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
35 changes: 0 additions & 35 deletions src/class-registry.test.ts

This file was deleted.

67 changes: 2 additions & 65 deletions src/class-registry.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,4 @@
import { Registry } from './registry';
import { Class } from './types';

class DoubleIndexedKV<K, V> {
keyToValue = new Map<K, V>();
valueToKey = new Map<V, K>();

set(key: K, value: V) {
this.keyToValue.set(key, value);
this.valueToKey.set(value, key);
}

deleteByValue(value: V) {
this.valueToKey.delete(value);
this.keyToValue.forEach((otherValue, otherKey) => {
if (value === otherValue) {
this.keyToValue.delete(otherKey);
}
});
}

getByKey(key: K): V | undefined {
return this.keyToValue.get(key);
}

getByValue(value: V): K | undefined {
return this.valueToKey.get(value);
}

clear() {
this.keyToValue.clear();
this.valueToKey.clear();
}
}

const classRegistry = new DoubleIndexedKV<string, Class>();

export function registerClass(clazz: Class, identifier?: string): void {
if (classRegistry.getByValue(clazz)) {
return;
}

if (!identifier) {
identifier = clazz.name;
}

if (classRegistry.getByKey(identifier)) {
throw new Error('Ambiguous class, provide a unique identifier.');
}

classRegistry.set(identifier, clazz);
}

export function unregisterClass(clazz: Class): void {
classRegistry.deleteByValue(clazz);
}

export function clear(): void {
classRegistry.clear();
}

export function getIdentifier(clazz: Class) {
return classRegistry.getByValue(clazz);
}

export function getClass(identifier: string) {
return classRegistry.getByKey(identifier);
}
export const ClassRegistry = new Registry<Class>(c => c.name);
31 changes: 31 additions & 0 deletions src/double-indexed-kv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export class DoubleIndexedKV<K, V> {
keyToValue = new Map<K, V>();
valueToKey = new Map<V, K>();

set(key: K, value: V) {
this.keyToValue.set(key, value);
this.valueToKey.set(value, key);
}

deleteByValue(value: V) {
this.valueToKey.delete(value);
this.keyToValue.forEach((otherValue, otherKey) => {
if (value === otherValue) {
this.keyToValue.delete(otherKey);
}
});
}

getByKey(key: K): V | undefined {
return this.keyToValue.get(key);
}

getByValue(value: V): K | undefined {
return this.valueToKey.get(value);
}

clear() {
this.keyToValue.clear();
this.valueToKey.clear();
}
}
28 changes: 26 additions & 2 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,30 @@ describe('stringify & parse', () => {
expect(value.users.values().next().value).toBe(value.userOfTheMonth);
},
},

'works for symbols': {
input: () => {
const parent = Symbol('Parent');
const child = Symbol('Child');
SuperJSON.registerSymbol(parent, '1');
SuperJSON.registerSymbol(child, '2');

const a = { role: parent };
const b = { role: child };

return { a, b };
},
output: {
a: { role: 'Parent' },
b: { role: 'Child' },
},
outputAnnotations: {
values: {
'a.role': [['symbol', '1']],
'b.role': [['symbol', '2']],
},
},
},
};

function deepFreeze(object: any, alreadySeenObjects = new Set()) {
Expand Down Expand Up @@ -457,7 +481,7 @@ describe('stringify & parse', () => {
SuperJSON.registerClass(Train);

const { json, meta } = SuperJSON.serialize({
s7: new Train(100, 'yellow', 'Bombardier'),
s7: new Train(100, 'yellow', 'Bombardier') as any,
});

expect(json).toEqual({
Expand Down Expand Up @@ -494,7 +518,7 @@ describe('stringify & parse', () => {
SuperJSON.registerClass(Currency);

const { json, meta } = SuperJSON.serialize({
price: new Currency(100),
price: new Currency(100) as any,
});

expect(json).toEqual({
Expand Down
21 changes: 18 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { applyAnnotations, makeAnnotator } from './annotator';
import { isEmptyObject } from './is';
import { plainer } from './plainer';
import { SuperJSONResult, SuperJSONValue, isSuperJSONResult } from './types';
import { clear, registerClass, unregisterClass } from './class-registry';
import {
SuperJSONResult,
SuperJSONValue,
isSuperJSONResult,
Class,
} from './types';
import { ClassRegistry } from './class-registry';
import { SymbolRegistry } from './symbol-registry';

const serialize = (object: SuperJSONValue): SuperJSONResult => {
const { getAnnotations, annotator } = makeAnnotator();
Expand Down Expand Up @@ -38,12 +44,21 @@ const stringify = (object: SuperJSONValue): string =>
export const parse = <T = unknown>(string: string): T =>
deserialize(JSON.parse(string));

const registerClass = (v: Class, identifier?: string) =>
ClassRegistry.register(v, identifier);
const unregisterClass = (v: Class) => ClassRegistry.unregister(v);

const registerSymbol = (v: Symbol, identifier?: string) =>
SymbolRegistry.register(v, identifier);
const unregisterSymbol = (v: Symbol) => SymbolRegistry.unregister(v);

export default {
stringify,
parse,
serialize,
deserialize,
clear,
registerClass,
unregisterClass,
registerSymbol,
unregisterSymbol,
};
38 changes: 38 additions & 0 deletions src/registry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Registry } from './registry';
import { Class } from './types';

test('class registry', () => {
const registry = new Registry<Class>(c => c.name);

class Car {
honk() {
console.log('honk');
}
}
registry.register(Car);

expect(registry.getValue('Car')).toBe(Car);
expect(registry.getIdentifier(Car)).toBe('Car');

expect(() => registry.register(Car)).not.toThrow();

expect(() => registry.register(class Car {})).toThrow(
'Ambiguous class, provide a unique identifier.'
);

registry.unregister(Car);

expect(registry.getValue('Car')).toBeUndefined();

registry.register(Car, 'car1');

registry.register(class Car {}, 'car2');

expect(registry.getValue('car1')).toBe(Car);

expect(registry.getValue('car2')).not.toBeUndefined();

registry.clear();

expect(registry.getValue('car1')).toBeUndefined();
});
39 changes: 39 additions & 0 deletions src/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { DoubleIndexedKV } from './double-indexed-kv';

export class Registry<T> {
private kv = new DoubleIndexedKV<string, T>();

constructor(private readonly generateIdentifier: (v: T) => string) {}

register(value: T, identifier?: string): void {
if (this.kv.getByValue(value)) {
return;
}

if (!identifier) {
identifier = this.generateIdentifier(value);
}

if (this.kv.getByKey(identifier)) {
throw new Error('Ambiguous class, provide a unique identifier.');
}

this.kv.set(identifier, value);
}

unregister(v: T): void {
this.kv.deleteByValue(v);
}

clear(): void {
this.kv.clear();
}

getIdentifier(value: T) {
return this.kv.getByValue(value);
}

getValue(identifier: string) {
return this.kv.getByKey(identifier);
}
}
3 changes: 3 additions & 0 deletions src/symbol-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Registry } from './registry';

export const SymbolRegistry = new Registry<Symbol>(s => s.description ?? '');
32 changes: 29 additions & 3 deletions src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,24 @@ import {
isRegExp,
isSet,
isUndefined,
isSymbol,
} from './is';
import * as ClassRegistry from './class-registry';
import { ClassRegistry } from './class-registry';
import { SymbolRegistry } from './symbol-registry';
import * as IteratorUtils from './iteratorutils';

export type PrimitiveTypeAnnotation = 'number' | 'undefined' | 'bigint';

type LeafTypeAnnotation = PrimitiveTypeAnnotation | 'regexp' | 'Date';

type ClassTypeAnnotation = ['class', string];
type SymbolTypeAnnotation = ['symbol', string];

type ContainerTypeAnnotation = 'map' | 'set' | ClassTypeAnnotation;
type ContainerTypeAnnotation =
| 'map'
| 'set'
| ClassTypeAnnotation
| SymbolTypeAnnotation;

export type TypeAnnotation = LeafTypeAnnotation | ContainerTypeAnnotation;

Expand All @@ -42,6 +49,7 @@ export const isTypeAnnotation = (value: any): value is TypeAnnotation => {
switch (value[0]) {
case 'map':
return ['number', 'string', 'bigint', 'boolean'].includes(value[1]);
case 'symbol':
case 'class':
return typeof value[1] === 'string';
}
Expand All @@ -58,6 +66,14 @@ export const transformValue = (
value: undefined,
type: 'undefined',
};
} else if (isSymbol(value)) {
const identifier = SymbolRegistry.getIdentifier(value);
if (identifier) {
return {
value: value.description,
type: ['symbol', identifier],
};
}
} else if (isBigint(value)) {
return {
value: value.toString(),
Expand Down Expand Up @@ -113,14 +129,24 @@ export const untransformValue = (json: any, type: TypeAnnotation) => {
if (Array.isArray(type)) {
switch (type[0]) {
case 'class': {
const clazz = ClassRegistry.getClass(type[1]);
const clazz = ClassRegistry.getValue(type[1]);

if (!clazz) {
throw new Error('Trying to deserialize unknown class');
}

return Object.assign(Object.create(clazz.prototype), json);
}

case 'symbol': {
const symbol = SymbolRegistry.getValue(type[1]);

if (!symbol) {
throw new Error('Trying to deserialize unknown symbol');
}

return symbol;
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface JSONObject {
type ClassInstance = any;

export type SerializableJSONValue =
| Symbol
| Set<SuperJSONValue>
| Map<SuperJSONValue, SuperJSONValue>
| undefined
Expand Down