diff --git a/src/class-registry.test.ts b/src/class-registry.test.ts deleted file mode 100644 index 4e6e410..0000000 --- a/src/class-registry.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as ClassRegistry from './class-registry'; - -test('class registry', () => { - class Car { - honk() { - console.log('honk'); - } - } - ClassRegistry.registerClass(Car); - - expect(ClassRegistry.getClass('Car')).toBe(Car); - expect(ClassRegistry.getIdentifier(Car)).toBe('Car'); - - expect(() => ClassRegistry.registerClass(Car)).not.toThrow(); - - expect(() => ClassRegistry.registerClass(class Car {})).toThrow( - 'Ambiguous class, provide a unique identifier.' - ); - - ClassRegistry.unregisterClass(Car); - - expect(ClassRegistry.getClass('Car')).toBeUndefined(); - - ClassRegistry.registerClass(Car, 'car1'); - - ClassRegistry.registerClass(class Car {}, 'car2'); - - expect(ClassRegistry.getClass('car1')).toBe(Car); - - expect(ClassRegistry.getClass('car2')).not.toBeUndefined(); - - ClassRegistry.clear(); - - expect(ClassRegistry.getClass('car1')).toBeUndefined(); -}); diff --git a/src/class-registry.ts b/src/class-registry.ts index 0ed301c..1f12fd0 100644 --- a/src/class-registry.ts +++ b/src/class-registry.ts @@ -1,67 +1,4 @@ +import { Registry } from './registry'; import { Class } from './types'; -class DoubleIndexedKV { - keyToValue = new Map(); - valueToKey = new Map(); - - 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(); - -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(c => c.name); diff --git a/src/double-indexed-kv.ts b/src/double-indexed-kv.ts new file mode 100644 index 0000000..9e348f0 --- /dev/null +++ b/src/double-indexed-kv.ts @@ -0,0 +1,31 @@ +export class DoubleIndexedKV { + keyToValue = new Map(); + valueToKey = new Map(); + + 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(); + } +} diff --git a/src/index.test.ts b/src/index.test.ts index 4bfb47f..d9bb14f 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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()) { @@ -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({ @@ -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({ diff --git a/src/index.ts b/src/index.ts index 2364ca3..5b7e522 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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(); @@ -38,12 +44,21 @@ const stringify = (object: SuperJSONValue): string => export const parse = (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, }; diff --git a/src/registry.test.ts b/src/registry.test.ts new file mode 100644 index 0000000..3c8de46 --- /dev/null +++ b/src/registry.test.ts @@ -0,0 +1,38 @@ +import { Registry } from './registry'; +import { Class } from './types'; + +test('class registry', () => { + const registry = new Registry(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(); +}); diff --git a/src/registry.ts b/src/registry.ts new file mode 100644 index 0000000..99e476f --- /dev/null +++ b/src/registry.ts @@ -0,0 +1,39 @@ +import { DoubleIndexedKV } from './double-indexed-kv'; + +export class Registry { + private kv = new DoubleIndexedKV(); + + 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); + } +} diff --git a/src/symbol-registry.ts b/src/symbol-registry.ts new file mode 100644 index 0000000..058683d --- /dev/null +++ b/src/symbol-registry.ts @@ -0,0 +1,3 @@ +import { Registry } from './registry'; + +export const SymbolRegistry = new Registry(s => s.description ?? ''); diff --git a/src/transformer.ts b/src/transformer.ts index 4517520..db31c4e 100644 --- a/src/transformer.ts +++ b/src/transformer.ts @@ -7,8 +7,10 @@ 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'; @@ -16,8 +18,13 @@ 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; @@ -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'; } @@ -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(), @@ -113,7 +129,7 @@ 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'); @@ -121,6 +137,16 @@ export const untransformValue = (json: any, type: TypeAnnotation) => { 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; + } } } diff --git a/src/types.ts b/src/types.ts index 647222e..a13bc69 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,7 @@ export interface JSONObject { type ClassInstance = any; export type SerializableJSONValue = + | Symbol | Set | Map | undefined