diff --git a/spec/inheritance-circular-references/barrel.ts b/spec/inheritance-circular-references/barrel.ts new file mode 100644 index 0000000..bea93d6 --- /dev/null +++ b/spec/inheritance-circular-references/barrel.ts @@ -0,0 +1,3 @@ +export * from './person.model'; +export * from './employee.model'; +export * from './part-time-employee.model'; diff --git a/spec/inheritance-circular-references/company.model.ts b/spec/inheritance-circular-references/company.model.ts new file mode 100644 index 0000000..047bbf5 --- /dev/null +++ b/spec/inheritance-circular-references/company.model.ts @@ -0,0 +1,9 @@ +import {jsonMember, jsonObject} from '../../src'; +import {Person} from './barrel'; + +@jsonObject +export class Company { + + @jsonMember + owner: Person; +} diff --git a/spec/inheritance-circular-references/employee.model.ts b/spec/inheritance-circular-references/employee.model.ts new file mode 100644 index 0000000..711ae89 --- /dev/null +++ b/spec/inheritance-circular-references/employee.model.ts @@ -0,0 +1,6 @@ +import {discriminatorProperty, jsonInheritance, jsonObject} from '../../src'; +import {Person} from './barrel'; + +@jsonObject +export class Employee extends Person { +} diff --git a/spec/inheritance-circular-references/inheritance-circular-references.spec.ts b/spec/inheritance-circular-references/inheritance-circular-references.spec.ts new file mode 100644 index 0000000..8c15f41 --- /dev/null +++ b/spec/inheritance-circular-references/inheritance-circular-references.spec.ts @@ -0,0 +1,16 @@ +import {TypedJSON} from '../../src'; +import {Company} from './company.model'; +import {PartTimeEmployee} from './part-time-employee.model'; + +describe('inheritance with circular references', () => { + it('should deserialize correctly', () => { + const result = TypedJSON.parse({ + owner: { + name: 'George', + type: 'PartTimeEmployee', + }, + }, Company); + + expect(result.owner).toBeInstanceOf(PartTimeEmployee); + }); +}); diff --git a/spec/inheritance-circular-references/part-time-employee.model.ts b/spec/inheritance-circular-references/part-time-employee.model.ts new file mode 100644 index 0000000..4b34c5c --- /dev/null +++ b/spec/inheritance-circular-references/part-time-employee.model.ts @@ -0,0 +1,6 @@ +import {jsonObject} from '../../src'; +import {Employee} from './barrel'; + +@jsonObject +export class PartTimeEmployee extends Employee { +} diff --git a/spec/inheritance-circular-references/person.model.ts b/spec/inheritance-circular-references/person.model.ts new file mode 100644 index 0000000..bb54bb3 --- /dev/null +++ b/spec/inheritance-circular-references/person.model.ts @@ -0,0 +1,16 @@ +import {discriminatorProperty, jsonInheritance, jsonMember, jsonObject} from '../../src'; +import {Employee, PartTimeEmployee} from './barrel'; + +@jsonInheritance(discriminatorProperty({ + property: 'type', + types: () => ({ + Employee: Employee, + PartTimeEmployee: PartTimeEmployee, + }), +})) +@jsonObject +export class Person { + + @jsonMember + name: string; +} diff --git a/spec/json-inheritance.spec.ts b/spec/json-inheritance.spec.ts new file mode 100644 index 0000000..843951e --- /dev/null +++ b/spec/json-inheritance.spec.ts @@ -0,0 +1,199 @@ +import { + discriminatorProperty, + jsonInheritance, + jsonMember, + jsonObject, + TypedJSON, +} from '../src'; + +describe('jsonInheritance', () => { + describe('with custom resolvers', () => { + @jsonInheritance({ + onSerializeType: (source, result) => { + result['test'] = 'hey'; + return result; + }, + resolveType: data => { + if ('badgeId' in data) { + return Employee; + } else if ('capital' in data) { + return Investor; + } + + return Person; + }, + }) + @jsonObject + class Person { + name: string; + } + + @jsonObject + class Employee extends Person { + badgeId: string; + } + + @jsonObject + class Investor extends Person { + capital: number; + } + + @jsonObject + class Company { + + @jsonMember + owner: Person; + } + + const typedJson = new TypedJSON(Company); + + it('should use resolveType', () => { + const result = typedJson.parse({owner: {name: 'Bob', badgeId: 'avx5'}}); + expect(result.owner).toBeInstanceOf(Employee); + }); + + it('should use onSerializeType', () => { + const company = new Company(); + company.owner = new Employee(); + company.owner.name = 'Lewis'; + const result: any = typedJson.toPlainJson(company); + expect(result.owner.test).toEqual('hey'); + }); + + it('onSerializeType should not modify source object', () => { + const company = new Company(); + company.owner = new Employee(); + typedJson.toPlainJson(company); + expect((company.owner as any).test).toBeUndefined(); + }); + }); + + describe('with discriminator', () => { + describe('and one jsonInheritance decorator', () => { + @jsonInheritance(discriminatorProperty({ + property: 'type', + types: () => ({ + Employee: Employee, + Investor: Investor, + PartTimeEmployee: PartTimeEmployee, + }), + })) + @jsonObject + class Person { + name: string; + } + + @jsonObject + class Employee extends Person { + } + + @jsonObject + class PartTimeEmployee extends Employee { + } + + @jsonObject + class Investor extends Person { + } + + @jsonObject + class Company { + + @jsonMember + owner: Person; + } + + it('should deserialize into correct type', () => { + const result = TypedJSON.parse({ + owner: { + name: 'Jeff', + type: 'Investor', + }, + }, Company); + + expect(result.owner).toBeInstanceOf(Investor); + }); + + it('should have correct type property on serialization', () => { + const company = new Company(); + company.owner = new Investor(); + + const result: any = TypedJSON.toPlainJson(company, Company); + expect(result.owner.type).toEqual('Investor'); + }); + + describe('and nested inheritance', () => { + it('should deserialize into correct type', () => { + const result = TypedJSON.parse({ + owner: { + name: 'George', + type: 'PartTimeEmployee', + }, + }, Company); + + expect(result.owner).toBeInstanceOf(PartTimeEmployee); + }); + + it('should have correct type property on serialization', () => { + const company = new Company(); + company.owner = new PartTimeEmployee(); + + const result: any = TypedJSON.toPlainJson(company, Company); + expect(result.owner.type).toEqual('PartTimeEmployee'); + }); + }); + }); + + describe('and multiple jsonInheritance decorators', () => { + @jsonInheritance(discriminatorProperty({ + property: 'type', + types: () => ({ + Employee: Employee, + }), + })) + @jsonObject + class Person { + name: string; + } + + @jsonInheritance(discriminatorProperty({ + property: 'type', + types: () => ({ + PartTimeEmployee: PartTimeEmployee, + }), + })) + @jsonObject + class Employee extends Person { + } + + @jsonObject + class PartTimeEmployee extends Employee { + } + + @jsonObject + class Company { + + @jsonMember + owner: Person; + } + + it('should deserialize into correct type', () => { + const result = TypedJSON.parse({ + owner: { + name: 'George', + type: 'PartTimeEmployee', + }, + }, Company); + + expect(result.owner).toBeInstanceOf(PartTimeEmployee); + }); + + it('should have correct type property on serialization', () => { + const company = new Company(); + company.owner = new PartTimeEmployee(); + + const result: any = TypedJSON.toPlainJson(company, Company); + expect(result.owner.type).toEqual('PartTimeEmployee'); + }); + }); + }); +}); diff --git a/spec/lazy-types/polymorphism-abstract-class.spec.ts b/spec/lazy-types/polymorphism-abstract-class.spec.ts index f9de480..4744671 100644 --- a/spec/lazy-types/polymorphism-abstract-class.spec.ts +++ b/spec/lazy-types/polymorphism-abstract-class.spec.ts @@ -1,7 +1,17 @@ -import {jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../../src'; +import {jsonArrayMember, jsonInheritance, jsonMember, jsonObject, TypedJSON} from '../../src'; import {isEqual} from '../utils/object-compare'; describe('lazy, polymorphic abstract classes', () => { + @jsonInheritance({ + resolveType: data => { + if ('inputType' in data) { + return SmallNode; + } else { + return BigNode; + } + }, + }) + @jsonObject() abstract class Node { @jsonMember name: string; @@ -31,11 +41,9 @@ describe('lazy, polymorphic abstract classes', () => { } } - @jsonObject({ - knownTypes: [BigNode, SmallNode], - }) + @jsonObject() class Graph { - @jsonArrayMember(() => Node) + @jsonArrayMember(Node) nodes: Array; @jsonMember @@ -46,8 +54,6 @@ describe('lazy, polymorphic abstract classes', () => { } } - let portTypeIndex = 0; - function randPortType() { const types = [ 'string', @@ -57,7 +63,7 @@ describe('lazy, polymorphic abstract classes', () => { 'void', ]; - return types[portTypeIndex++ % types.length]; + return types[Math.floor(Math.random() * types.length)]; } function test(log: boolean) { @@ -66,7 +72,7 @@ describe('lazy, polymorphic abstract classes', () => { for (let i = 0; i < 20; i++) { let node: Node; - if (i % 2 === 0) { + if (Math.random() < 0.25) { const bigNode = new BigNode(); bigNode.inputs = [ diff --git a/spec/lazy-types/polymorphism-custom-names.spec.ts b/spec/lazy-types/polymorphism-custom-names.spec.ts index b981e97..16c5926 100644 --- a/spec/lazy-types/polymorphism-custom-names.spec.ts +++ b/spec/lazy-types/polymorphism-custom-names.spec.ts @@ -1,8 +1,17 @@ -import {jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../../src'; +import {jsonArrayMember, jsonInheritance, jsonMember, jsonObject, TypedJSON} from '../../src'; import {isEqual} from '../utils/object-compare'; describe('lazy, polymorphic custom names', () => { - @jsonObject + @jsonInheritance({ + resolveType: data => { + if ('invest-amount' in data) { + return Investor; + } + + return Employee; + }, + }) + @jsonObject() class Person { @jsonMember({name: 'first-name'}) firstName: string; @@ -18,7 +27,16 @@ describe('lazy, polymorphic custom names', () => { } } - @jsonObject + @jsonInheritance({ + resolveType: data => { + if ('work-hours' in data) { + return PartTimeEmployee; + } + + return Employee; + }, + }) + @jsonObject() class Employee extends Person { @jsonMember salary: number; @@ -59,7 +77,7 @@ describe('lazy, polymorphic custom names', () => { } } - @jsonObject({name: 'company', knownTypes: [PartTimeEmployee, Investor]}) + @jsonObject({name: 'company'}) class Company { @jsonMember name: string; diff --git a/spec/lazy-types/polymorphism-custom-type-hints.spec.ts b/spec/lazy-types/polymorphism-custom-type-hints.spec.ts deleted file mode 100644 index 4dee868..0000000 --- a/spec/lazy-types/polymorphism-custom-type-hints.spec.ts +++ /dev/null @@ -1,316 +0,0 @@ -import {jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../../src'; -import {IndexedObject} from '../../src/types'; - -describe('lazy, polymorphism custom type hints', () => { - describe('should work for a base class', () => { - @jsonObject({ - typeHintEmitter: (targetObject, sourceObject) => { - targetObject.personType = `${sourceObject.constructor.name}Type`; - }, - typeResolver: sourceObject => TYPE_MAP[sourceObject.personType], - }) - abstract class Person { - @jsonMember - firstName: string; - - @jsonMember - lastName: string; - - constructor(firstName?: string, lastName?: string); - constructor(firstName: string, lastName: string); - constructor(firstName?: string, lastName?: string) { - if (firstName !== undefined && lastName !== undefined) { - this.firstName = firstName; - this.lastName = lastName; - } - } - } - - @jsonObject - class Employee extends Person { - @jsonMember - salary: number; - - constructor(); - constructor(firstName: string, lastName: string, salary?: number); - constructor(firstName?: string, lastName?: string, salary?: number) { - super(firstName, lastName); - - if (salary !== undefined) { - this.salary = salary; - } - } - } - - @jsonObject - class PartTimeEmployee extends Employee { - @jsonMember - workHours: number; - } - - @jsonObject - class Investor extends Person { - @jsonMember - investAmount: number; - - constructor(); - constructor(firstName: string, lastName: string, investAmount?: number); - constructor(firstName?: string, lastName?: string, investAmount?: number) { - super(firstName, lastName); - - this.investAmount = investAmount ?? 0; - } - } - - const TYPE_MAP: IndexedObject = { - EmployeeType: Employee, - PartTimeEmployeeType: PartTimeEmployee, - InvestorType: Investor, - }; - - @jsonObject - class Company { - @jsonMember - name: string; - - @jsonArrayMember(() => Employee) - employees: Array = []; - - @jsonMember - owner: Person; - } - - it('should emit custom hint', () => { - const company = new Company(); - company.name = 'Json Types'; - company.owner = new Investor('John', 'White', 1700000); - - const partTime = new PartTimeEmployee('Abe', 'White', 160000); - partTime.workHours = 20; - company.employees = [ - new Employee('Donn', 'Worker', 240000), - partTime, - new Employee('Smith', 'Elly', 35500), - ]; - - const json = TypedJSON.toPlainJson(company, Company); - expect(json).toEqual({ - name: 'Json Types', - owner: { - personType: 'InvestorType', - firstName: 'John', - lastName: 'White', - investAmount: 1700000, - }, - employees: [ - { - personType: 'EmployeeType', - firstName: 'Donn', - lastName: 'Worker', - salary: 240000, - }, - { - personType: 'PartTimeEmployeeType', - firstName: 'Abe', - lastName: 'White', - salary: 160000, - workHours: 20, - }, - { - personType: 'EmployeeType', - firstName: 'Smith', - lastName: 'Elly', - salary: 35500, - }, - ], - }); - }); - - it('should resolve custom hints', () => { - const json = { - name: 'Json Types', - owner: { - personType: 'InvestorType', - firstName: 'John', - lastName: 'White', - investAmount: 1700000, - }, - employees: [ - { - personType: 'EmployeeType', - firstName: 'Donn', - lastName: 'Worker', - salary: 240000, - }, - { - personType: 'PartTimeEmployeeType', - firstName: 'Abe', - lastName: 'White', - salary: 160000, - workHours: 20, - }, - { - personType: 'EmployeeType', - firstName: 'Smith', - lastName: 'Elly', - salary: 35500, - }, - ], - }; - - const deserialized = TypedJSON.parse(JSON.stringify(json), Company); - - const company = new Company(); - company.name = 'Json Types'; - company.owner = new Investor('John', 'White', 1700000); - - const partTime = new PartTimeEmployee('Abe', 'White', 160000); - partTime.workHours = 20; - company.employees = [ - new Employee('Donn', 'Worker', 240000), - partTime, - new Employee('Smith', 'Elly', 35500), - ]; - expect(deserialized).toEqual(company); - }); - }); - - describe('should override parents', () => { - abstract class StructuralBase { - @jsonMember - value: string; - } - - @jsonObject({ - typeHintEmitter: (targetObject, sourceObject) => { - targetObject.type = (sourceObject.constructor as any).type; - }, - typeResolver: sourceObject => { - return sourceObject.type === 'sub-one' ? ConcreteOne : AnotherConcreteOne; - }, - }) - abstract class SemanticBaseOne extends StructuralBase { - @jsonMember - prop1: number; - } - - @jsonObject - class ConcreteOne extends SemanticBaseOne { - static type = 'sub-one'; - @jsonMember - propSub: string; - } - - @jsonObject - class AnotherConcreteOne extends SemanticBaseOne { - static type = 'sub-two'; - @jsonMember - propSub: number; - } - - @jsonObject({ - typeHintEmitter: (targetObject, sourceObject) => { - targetObject.hint = sourceObject instanceof ConcreteTwo ? 'first' : 'another'; - }, - typeResolver: sourceObject => { - return sourceObject.hint === 'first' ? ConcreteTwo : AnotherConcreteTwo; - }, - }) - abstract class SemanticBaseTwo extends StructuralBase { - @jsonMember - prop2: number; - } - - @jsonObject - class ConcreteTwo extends SemanticBaseTwo { - @jsonMember - propSub: string; - } - - @jsonObject - class AnotherConcreteTwo extends SemanticBaseTwo { - @jsonMember - propSub: number; - } - - it('should work for SemanticBaseOne', () => { - const inputAndResult: Array<[() => SemanticBaseOne, () => IndexedObject]> = [ - [ - () => { - const expected = new ConcreteOne(); - expected.value = 'base'; - expected.prop1 = 10; - expected.propSub = 'something'; - return expected; - }, - () => ({ - type: 'sub-one', - value: 'base', - prop1: 10, - propSub: 'something', - }), - ], - [ - () => { - const expected = new AnotherConcreteOne(); - expected.value = 'base value'; - expected.prop1 = 245; - expected.propSub = 234; - return expected; - }, - () => ({ - type: 'sub-two', - value: 'base value', - prop1: 245, - propSub: 234, - }), - ], - ]; - - inputAndResult.forEach(([inputFn, serializedFn]) => { - expect(TypedJSON.toPlainJson(inputFn(), SemanticBaseOne)).toEqual(serializedFn()); - expect(TypedJSON.parse(serializedFn(), SemanticBaseOne)).toEqual(inputFn()); - }); - }); - - it('should work for SemanticBaseTwo', () => { - const inputAndResult: Array<[() => SemanticBaseTwo, () => IndexedObject]> = [ - [ - () => { - const expected = new ConcreteTwo(); - expected.value = 'base'; - expected.prop2 = 546; - expected.propSub = 'something'; - return expected; - }, - () => ({ - hint: 'first', - value: 'base', - prop2: 546, - propSub: 'something', - }), - ], - [ - () => { - const expected = new AnotherConcreteTwo(); - expected.value = 'base value'; - expected.prop2 = 74; - expected.propSub = 234; - return expected; - }, - () => ({ - hint: 'another', - value: 'base value', - prop2: 74, - propSub: 234, - }), - ], - ]; - - inputAndResult.forEach(([inputFn, serializedFn]) => { - expect(TypedJSON.toPlainJson(inputFn(), SemanticBaseTwo)).toEqual(serializedFn()); - expect(TypedJSON.parse(serializedFn(), SemanticBaseTwo)).toEqual(inputFn()); - }); - }); - }); -}); diff --git a/spec/lazy-types/polymorphism-interface.spec.ts b/spec/lazy-types/polymorphism-interface.spec.ts deleted file mode 100644 index 95e0d85..0000000 --- a/spec/lazy-types/polymorphism-interface.spec.ts +++ /dev/null @@ -1,130 +0,0 @@ -import {AnyT, jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../../src'; -import {isEqual} from '../utils/object-compare'; - -describe('lazy, polymorphic interfaces', () => { - interface Point { - x: number; - y: number; - } - - @jsonObject - class SmallNode implements Point { - @jsonMember - x: number; - - @jsonMember - y: number; - - @jsonMember - inputType: string; - - @jsonMember - outputType: string; - } - - @jsonObject - class BigNode implements Point { - @jsonMember - x: number; - - @jsonMember - y: number; - - @jsonArrayMember(() => String) - inputs: Array; - - @jsonArrayMember(() => String) - outputs: Array; - - constructor() { - this.inputs = []; - this.outputs = []; - } - } - - @jsonObject({ - knownTypes: [BigNode, SmallNode], - }) - class GraphGrid { - @jsonArrayMember(() => AnyT) - points: Array; - - @jsonMember - root: Point; - - constructor() { - this.points = []; - } - } - - let portTypeIndex = 0; - - function randPortType() { - const types = [ - 'string', - 'integer', - 'float', - 'boolean', - 'void', - ]; - - return types[portTypeIndex++ % types.length]; - } - - function test(log: boolean) { - const graph = new GraphGrid(); - - for (let i = 0; i < 20; i++) { - let point: Point; - - if (i % 2 === 0) { - const bigNode = new BigNode(); - - bigNode.inputs = [ - randPortType(), - randPortType(), - randPortType(), - ]; - bigNode.outputs = [ - randPortType(), - randPortType(), - ]; - - point = bigNode; - } else { - const smallNode = new SmallNode(); - - smallNode.inputType = randPortType(); - smallNode.outputType = randPortType(); - - point = smallNode; - } - - point.x = Math.random(); - point.y = Math.random(); - - if (i === 0) { - graph.root = point; - } else { - graph.points.push(point); - } - } - - const json = TypedJSON.stringify(graph, GraphGrid); - const clone = TypedJSON.parse(json, GraphGrid); - - if (log) { - console.log('Test: polymorphism with interface property types...'); - console.log(graph); - console.log(JSON.parse(json)); - console.log(clone); - console.log('Test finished.'); - } - - return isEqual(graph, clone); - } - - it('should work', () => { - expect(test(false)).toBeTruthy(); - }); -}); diff --git a/spec/lazy-types/polymorphism-nested-arrays.spec.ts b/spec/lazy-types/polymorphism-nested-arrays.spec.ts index 83ea183..f507782 100644 --- a/spec/lazy-types/polymorphism-nested-arrays.spec.ts +++ b/spec/lazy-types/polymorphism-nested-arrays.spec.ts @@ -1,7 +1,17 @@ -import {jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../../src'; +import {jsonArrayMember, jsonInheritance, jsonMember, jsonObject, TypedJSON} from '../../src'; import {isEqual} from '../utils/object-compare'; describe('lazy, polymorphism in nested arrays', () => { + @jsonInheritance({ + resolveType: data => { + if ('inputType' in data) { + return SmallNode; + } else { + return BigNode; + } + }, + }) + @jsonObject abstract class Node { @jsonMember name: string; @@ -31,7 +41,7 @@ describe('lazy, polymorphism in nested arrays', () => { } } - @jsonObject({knownTypes: [BigNode, SmallNode]}) + @jsonObject class Graph { @jsonArrayMember(() => Node, {dimensions: 2}) items: Array>; diff --git a/spec/lazy-types/polymorphism.spec.ts b/spec/lazy-types/polymorphism.spec.ts index 46c62a9..8c1cf88 100644 --- a/spec/lazy-types/polymorphism.spec.ts +++ b/spec/lazy-types/polymorphism.spec.ts @@ -1,7 +1,16 @@ -import {jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../../src'; +import {jsonArrayMember, jsonInheritance, jsonMember, jsonObject, TypedJSON} from '../../src'; import {isEqual} from '../utils/object-compare'; describe('lazy, polymorphism', () => { + @jsonInheritance({ + resolveType: data => { + if ('investAmount' in data) { + return Investor; + } + + return Employee; + }, + }) @jsonObject class Person { @jsonMember @@ -20,6 +29,15 @@ describe('lazy, polymorphism', () => { } } + @jsonInheritance({ + resolveType: data => { + if ('workHours' in data) { + return PartTimeEmployee; + } else { + return Employee; + } + }, + }) @jsonObject class Employee extends Person { @jsonMember @@ -61,7 +79,7 @@ describe('lazy, polymorphism', () => { } } - @jsonObject({knownTypes: [PartTimeEmployee, Investor]}) + @jsonObject() class Company { @jsonMember name: string; diff --git a/spec/polymorphism-abstract-class.spec.ts b/spec/polymorphism-abstract-class.spec.ts index 23d5ffb..5ac067a 100644 --- a/spec/polymorphism-abstract-class.spec.ts +++ b/spec/polymorphism-abstract-class.spec.ts @@ -1,7 +1,17 @@ -import {jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../src'; +import {jsonArrayMember, jsonInheritance, jsonMember, jsonObject, TypedJSON} from '../src'; import {isEqual} from './utils/object-compare'; describe('polymorphic abstract classes', () => { + @jsonInheritance({ + resolveType: data => { + if ('inputType' in data) { + return SmallNode; + } else { + return BigNode; + } + }, + }) + @jsonObject() abstract class Node { @jsonMember name: string; @@ -31,9 +41,7 @@ describe('polymorphic abstract classes', () => { } } - @jsonObject({ - knownTypes: [BigNode, SmallNode], - }) + @jsonObject() class Graph { @jsonArrayMember(Node) nodes: Array; @@ -46,6 +54,8 @@ describe('polymorphic abstract classes', () => { } } + let portTypeIndex = 0; + function randPortType() { const types = [ 'string', @@ -55,7 +65,7 @@ describe('polymorphic abstract classes', () => { 'void', ]; - return types[Math.floor(Math.random() * types.length)]; + return types[portTypeIndex++ % types.length]; } function test(log: boolean) { @@ -64,7 +74,7 @@ describe('polymorphic abstract classes', () => { for (let i = 0; i < 20; i++) { let node: Node; - if (Math.random() < 0.25) { + if (i % 2 === 0) { const bigNode = new BigNode(); bigNode.inputs = [ diff --git a/spec/polymorphism-custom-names.spec.ts b/spec/polymorphism-custom-names.spec.ts index 7399494..9478b0c 100644 --- a/spec/polymorphism-custom-names.spec.ts +++ b/spec/polymorphism-custom-names.spec.ts @@ -1,8 +1,17 @@ -import {jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../src'; +import {jsonArrayMember, jsonInheritance, jsonMember, jsonObject, TypedJSON} from '../src'; import {isEqual} from './utils/object-compare'; describe('polymorphic custom names', () => { - @jsonObject + @jsonInheritance({ + resolveType: data => { + if ('invest-amount' in data) { + return Investor; + } + + return Employee; + }, + }) + @jsonObject() class Person { @jsonMember({name: 'first-name'}) firstName: string; @@ -18,7 +27,16 @@ describe('polymorphic custom names', () => { } } - @jsonObject + @jsonInheritance({ + resolveType: data => { + if ('work-hours' in data) { + return PartTimeEmployee; + } + + return Employee; + }, + }) + @jsonObject() class Employee extends Person { @jsonMember salary: number; @@ -59,7 +77,7 @@ describe('polymorphic custom names', () => { } } - @jsonObject({name: 'company', knownTypes: [PartTimeEmployee, Investor]}) + @jsonObject({name: 'company'}) class Company { @jsonMember name: string; @@ -75,38 +93,15 @@ describe('polymorphic custom names', () => { } } - function test(log: boolean) { + function test(owner: Person) { // Create a Company. const company = new Company(); company.name = 'Json Types'; - - switch (Math.floor(Math.random() * 4)) { - case 0: - company.owner = new Employee('John', 'White', 240000, new Date(1992, 5, 27)); - break; - - case 1: - company.owner = new Investor('John', 'White', 1700000); - break; - - case 2: - company.owner = new PartTimeEmployee( - 'John', - 'White', - 160000, - new Date(1992, 5, 27), - ); - (company.owner as PartTimeEmployee).workHours = Math.floor(Math.random() * 40); - break; - - default: - company.owner = new Person('John', 'White'); - break; - } + company.owner = owner; // Add employees. for (let j = 0; j < 20; j++) { - if (Math.random() < 0.2) { + if (j % 2 === 0) { const newPartTimeEmployee = new PartTimeEmployee( `firstname_${j}`, `lastname_${j}`, @@ -130,18 +125,30 @@ describe('polymorphic custom names', () => { const json = TypedJSON.stringify(company, Company); const reparsed = TypedJSON.parse(json, Company); - if (log) { - console.log('Test: polymorphism with custom names...'); - console.log(company); - console.log(JSON.parse(json)); - console.log(reparsed); - console.log('Test finished.'); + const success = isEqual(company, reparsed); + + if (!success) { + console.log('Polymorphism test failed'); + console.log('company', company); + console.log('json', JSON.parse(json)); + console.log('reparsed', reparsed); } - return isEqual(company, reparsed); + return success; } it('should work', () => { - expect(test(false)).toBeTruthy(); + expect(test(new Employee('John', 'White', 240000, new Date(1992, 5, 27)))).toBeTruthy(); + expect(test(new Investor('John', 'White', 1700000))).toBeTruthy(); + const partTimeEmployee = new PartTimeEmployee( + 'John', + 'White', + 160000, + new Date(1992, 5, 27), + ); + + partTimeEmployee.workHours = 38; + expect(test(partTimeEmployee)).toBeTruthy(); + expect(test(new Person('John', 'White'))).toBeTruthy(); }); }); diff --git a/spec/polymorphism-custom-type-hints.spec.ts b/spec/polymorphism-custom-type-hints.spec.ts deleted file mode 100644 index 0d2c27c..0000000 --- a/spec/polymorphism-custom-type-hints.spec.ts +++ /dev/null @@ -1,316 +0,0 @@ -import {jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../src'; -import {IndexedObject} from '../src/types'; - -describe('polymorphism custom type hints', () => { - describe('should work for a base class', () => { - @jsonObject({ - typeHintEmitter: (targetObject, sourceObject) => { - targetObject.personType = `${sourceObject.constructor.name}Type`; - }, - typeResolver: sourceObject => TYPE_MAP[sourceObject.personType], - }) - abstract class Person { - @jsonMember - firstName: string; - - @jsonMember - lastName: string; - - constructor(firstName?: string, lastName?: string); - constructor(firstName: string, lastName: string); - constructor(firstName?: string, lastName?: string) { - if (firstName !== undefined && lastName !== undefined) { - this.firstName = firstName; - this.lastName = lastName; - } - } - } - - @jsonObject - class Employee extends Person { - @jsonMember - salary: number; - - constructor(); - constructor(firstName: string, lastName: string, salary?: number); - constructor(firstName?: string, lastName?: string, salary?: number) { - super(firstName, lastName); - - if (salary !== undefined) { - this.salary = salary; - } - } - } - - @jsonObject - class PartTimeEmployee extends Employee { - @jsonMember - workHours: number; - } - - @jsonObject - class Investor extends Person { - @jsonMember - investAmount: number; - - constructor(); - constructor(firstName: string, lastName: string, investAmount?: number); - constructor(firstName?: string, lastName?: string, investAmount?: number) { - super(firstName, lastName); - - this.investAmount = investAmount ?? 0; - } - } - - const TYPE_MAP: IndexedObject = { - EmployeeType: Employee, - PartTimeEmployeeType: PartTimeEmployee, - InvestorType: Investor, - }; - - @jsonObject - class Company { - @jsonMember - name: string; - - @jsonArrayMember(Employee) - employees: Array = []; - - @jsonMember - owner: Person; - } - - it('should emit custom hint', () => { - const company = new Company(); - company.name = 'Json Types'; - company.owner = new Investor('John', 'White', 1700000); - - const partTime = new PartTimeEmployee('Abe', 'White', 160000); - partTime.workHours = 20; - company.employees = [ - new Employee('Donn', 'Worker', 240000), - partTime, - new Employee('Smith', 'Elly', 35500), - ]; - - const json = TypedJSON.toPlainJson(company, Company); - expect(json).toEqual({ - name: 'Json Types', - owner: { - personType: 'InvestorType', - firstName: 'John', - lastName: 'White', - investAmount: 1700000, - }, - employees: [ - { - personType: 'EmployeeType', - firstName: 'Donn', - lastName: 'Worker', - salary: 240000, - }, - { - personType: 'PartTimeEmployeeType', - firstName: 'Abe', - lastName: 'White', - salary: 160000, - workHours: 20, - }, - { - personType: 'EmployeeType', - firstName: 'Smith', - lastName: 'Elly', - salary: 35500, - }, - ], - }); - }); - - it('should resolve custom hints', () => { - const json = { - name: 'Json Types', - owner: { - personType: 'InvestorType', - firstName: 'John', - lastName: 'White', - investAmount: 1700000, - }, - employees: [ - { - personType: 'EmployeeType', - firstName: 'Donn', - lastName: 'Worker', - salary: 240000, - }, - { - personType: 'PartTimeEmployeeType', - firstName: 'Abe', - lastName: 'White', - salary: 160000, - workHours: 20, - }, - { - personType: 'EmployeeType', - firstName: 'Smith', - lastName: 'Elly', - salary: 35500, - }, - ], - }; - - const deserialized = TypedJSON.parse(JSON.stringify(json), Company); - - const company = new Company(); - company.name = 'Json Types'; - company.owner = new Investor('John', 'White', 1700000); - - const partTime = new PartTimeEmployee('Abe', 'White', 160000); - partTime.workHours = 20; - company.employees = [ - new Employee('Donn', 'Worker', 240000), - partTime, - new Employee('Smith', 'Elly', 35500), - ]; - expect(deserialized).toEqual(company); - }); - }); - - describe('should override parents', () => { - abstract class StructuralBase { - @jsonMember - value: string; - } - - @jsonObject({ - typeHintEmitter: (targetObject, sourceObject) => { - targetObject.type = (sourceObject.constructor as any).type; - }, - typeResolver: sourceObject => { - return sourceObject.type === 'sub-one' ? ConcreteOne : AnotherConcreteOne; - }, - }) - abstract class SemanticBaseOne extends StructuralBase { - @jsonMember - prop1: number; - } - - @jsonObject - class ConcreteOne extends SemanticBaseOne { - static type = 'sub-one'; - @jsonMember - propSub: string; - } - - @jsonObject - class AnotherConcreteOne extends SemanticBaseOne { - static type = 'sub-two'; - @jsonMember - propSub: number; - } - - @jsonObject({ - typeHintEmitter: (targetObject, sourceObject) => { - targetObject.hint = sourceObject instanceof ConcreteTwo ? 'first' : 'another'; - }, - typeResolver: sourceObject => { - return sourceObject.hint === 'first' ? ConcreteTwo : AnotherConcreteTwo; - }, - }) - abstract class SemanticBaseTwo extends StructuralBase { - @jsonMember - prop2: number; - } - - @jsonObject - class ConcreteTwo extends SemanticBaseTwo { - @jsonMember - propSub: string; - } - - @jsonObject - class AnotherConcreteTwo extends SemanticBaseTwo { - @jsonMember - propSub: number; - } - - it('should work for SemanticBaseOne', () => { - const inputAndResult: Array<[() => SemanticBaseOne, () => IndexedObject]> = [ - [ - () => { - const expected = new ConcreteOne(); - expected.value = 'base'; - expected.prop1 = 10; - expected.propSub = 'something'; - return expected; - }, - () => ({ - type: 'sub-one', - value: 'base', - prop1: 10, - propSub: 'something', - }), - ], - [ - () => { - const expected = new AnotherConcreteOne(); - expected.value = 'base value'; - expected.prop1 = 245; - expected.propSub = 234; - return expected; - }, - () => ({ - type: 'sub-two', - value: 'base value', - prop1: 245, - propSub: 234, - }), - ], - ]; - - inputAndResult.forEach(([inputFn, serializedFn]) => { - expect(TypedJSON.toPlainJson(inputFn(), SemanticBaseOne)).toEqual(serializedFn()); - expect(TypedJSON.parse(serializedFn(), SemanticBaseOne)).toEqual(inputFn()); - }); - }); - - it('should work for SemanticBaseTwo', () => { - const inputAndResult: Array<[() => SemanticBaseTwo, () => IndexedObject]> = [ - [ - () => { - const expected = new ConcreteTwo(); - expected.value = 'base'; - expected.prop2 = 546; - expected.propSub = 'something'; - return expected; - }, - () => ({ - hint: 'first', - value: 'base', - prop2: 546, - propSub: 'something', - }), - ], - [ - () => { - const expected = new AnotherConcreteTwo(); - expected.value = 'base value'; - expected.prop2 = 74; - expected.propSub = 234; - return expected; - }, - () => ({ - hint: 'another', - value: 'base value', - prop2: 74, - propSub: 234, - }), - ], - ]; - - inputAndResult.forEach(([inputFn, serializedFn]) => { - expect(TypedJSON.toPlainJson(inputFn(), SemanticBaseTwo)).toEqual(serializedFn()); - expect(TypedJSON.parse(serializedFn(), SemanticBaseTwo)).toEqual(inputFn()); - }); - }); - }); -}); diff --git a/spec/polymorphism-interface.spec.ts b/spec/polymorphism-interface.spec.ts deleted file mode 100644 index 4ea400e..0000000 --- a/spec/polymorphism-interface.spec.ts +++ /dev/null @@ -1,128 +0,0 @@ -import {jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../src'; -import {isEqual} from './utils/object-compare'; - -describe('polymorphic interfaces', () => { - interface Point { - x: number; - y: number; - } - - @jsonObject - class SmallNode implements Point { - @jsonMember - x: number; - - @jsonMember - y: number; - - @jsonMember - inputType: string; - - @jsonMember - outputType: string; - } - - @jsonObject - class BigNode implements Point { - @jsonMember - x: number; - - @jsonMember - y: number; - - @jsonArrayMember(String) - inputs: Array; - - @jsonArrayMember(String) - outputs: Array; - - constructor() { - this.inputs = []; - this.outputs = []; - } - } - - @jsonObject({ - knownTypes: [BigNode, SmallNode], - }) - class GraphGrid { - @jsonArrayMember(Object) - points: Array; - - @jsonMember - root: Point; - - constructor() { - this.points = []; - } - } - - function randPortType() { - const types = [ - 'string', - 'integer', - 'float', - 'boolean', - 'void', - ]; - - return types[Math.floor(Math.random() * types.length)]; - } - - function test(log: boolean) { - const graph = new GraphGrid(); - - for (let i = 0; i < 20; i++) { - let point: Point; - - if (Math.random() < 0.25) { - const bigNode = new BigNode(); - - bigNode.inputs = [ - randPortType(), - randPortType(), - randPortType(), - ]; - bigNode.outputs = [ - randPortType(), - randPortType(), - ]; - - point = bigNode; - } else { - const smallNode = new SmallNode(); - - smallNode.inputType = randPortType(); - smallNode.outputType = randPortType(); - - point = smallNode; - } - - point.x = Math.random(); - point.y = Math.random(); - - if (i === 0) { - graph.root = point; - } else { - graph.points.push(point); - } - } - - const json = TypedJSON.stringify(graph, GraphGrid); - const clone = TypedJSON.parse(json, GraphGrid); - - if (log) { - console.log('Test: polymorphism with interface property types...'); - console.log(graph); - console.log(JSON.parse(json)); - console.log(clone); - console.log('Test finished.'); - } - - return isEqual(graph, clone); - } - - it('should work', () => { - expect(test(false)).toBeTruthy(); - }); -}); diff --git a/spec/polymorphism-nested-arrays.spec.ts b/spec/polymorphism-nested-arrays.spec.ts index 71c7168..3487bd0 100644 --- a/spec/polymorphism-nested-arrays.spec.ts +++ b/spec/polymorphism-nested-arrays.spec.ts @@ -1,7 +1,17 @@ -import {jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../src'; +import {jsonArrayMember, jsonInheritance, jsonMember, jsonObject, TypedJSON} from '../src'; import {isEqual} from './utils/object-compare'; describe('polymorphism in nested arrays', () => { + @jsonInheritance({ + resolveType: data => { + if ('inputType' in data) { + return SmallNode; + } else { + return BigNode; + } + }, + }) + @jsonObject abstract class Node { @jsonMember name: string; @@ -31,7 +41,7 @@ describe('polymorphism in nested arrays', () => { } } - @jsonObject({knownTypes: [BigNode, SmallNode]}) + @jsonObject class Graph { @jsonArrayMember(Node, {dimensions: 2}) items: Array>; @@ -45,6 +55,8 @@ describe('polymorphism in nested arrays', () => { } } + let portTypeIndex = 0; + function randPortType() { const types = [ 'string', @@ -54,7 +66,7 @@ describe('polymorphism in nested arrays', () => { 'void', ]; - return types[Math.floor(Math.random() * types.length)]; + return types[portTypeIndex++ % types.length]; } function test(log: boolean) { @@ -80,7 +92,7 @@ describe('polymorphism in nested arrays', () => { for (let j = 0; j < 8; j++) { let node: Node; - if (Math.random() < 0.25) { + if (j % 2 === 0) { const bigNode = new BigNode(); bigNode.inputs = [ diff --git a/spec/polymorphism-root-abstract-class.spec.ts b/spec/polymorphism-root-abstract-class.spec.ts index 0a4053d..1fff393 100644 --- a/spec/polymorphism-root-abstract-class.spec.ts +++ b/spec/polymorphism-root-abstract-class.spec.ts @@ -1,6 +1,12 @@ -import {jsonMember, jsonObject, TypedJSON} from '../src'; +import {jsonArrayMember, jsonInheritance, jsonMember, jsonObject, TypedJSON} from '../src'; describe('single class', () => { + @jsonInheritance({ + resolveType: data => { + return Bob; + }, + }) + @jsonObject abstract class Person { @jsonMember firstName?: string; @@ -23,13 +29,10 @@ describe('single class', () => { } } - // todo we need something better - jsonObject({knownTypes: [Bob]})(Person); - describe('deserialized', () => { beforeAll(function () { this.person = TypedJSON.parse( - '{ "__type": "Bob", "firstName": "John", "lastName": "Doe", "pounds": 40 }', + '{ "firstName": "John", "lastName": "Doe", "pounds": 40 }', Person, ); }); @@ -52,7 +55,7 @@ describe('single class', () => { person.pounds = 30; // todo fix types so they accept abstract expect(TypedJSON.stringify(person, Person)) - .toBe('{"firstName":"John","lastName":"Doe","pounds":30,"__type":"Bob"}'); + .toBe('{"firstName":"John","lastName":"Doe","pounds":30}'); }); }); }); diff --git a/spec/polymorphism.spec.ts b/spec/polymorphism.spec.ts index 6a0fd6e..52f9ef8 100644 --- a/spec/polymorphism.spec.ts +++ b/spec/polymorphism.spec.ts @@ -1,7 +1,16 @@ -import {jsonArrayMember, jsonMember, jsonObject, TypedJSON} from '../src'; +import {jsonArrayMember, jsonInheritance, jsonMember, jsonObject, TypedJSON} from '../src'; import {isEqual} from './utils/object-compare'; describe('polymorphism', () => { + @jsonInheritance({ + resolveType: data => { + if ('investAmount' in data) { + return Investor; + } + + return Employee; + }, + }) @jsonObject class Person { @jsonMember @@ -20,6 +29,15 @@ describe('polymorphism', () => { } } + @jsonInheritance({ + resolveType: data => { + if ('workHours' in data) { + return PartTimeEmployee; + } else { + return Employee; + } + }, + }) @jsonObject class Employee extends Person { @jsonMember @@ -61,7 +79,7 @@ describe('polymorphism', () => { } } - @jsonObject({knownTypes: [PartTimeEmployee, Investor]}) + @jsonObject() class Company { @jsonMember name: string; @@ -77,38 +95,15 @@ describe('polymorphism', () => { } } - function test(log: boolean) { + function test(owner: Person) { // Create a Company. const company = new Company(); company.name = 'Json Types'; - - switch (Math.floor(Math.random() * 4)) { - case 0: - company.owner = new Employee('John', 'White', 240000, new Date(1992, 5, 27)); - break; - - case 1: - company.owner = new Investor('John', 'White', 1700000); - break; - - case 2: - company.owner = new PartTimeEmployee( - 'John', - 'White', - 160000, - new Date(1992, 5, 27), - ); - (company.owner as PartTimeEmployee).workHours = Math.floor(Math.random() * 40); - break; - - default: - company.owner = new Person('John', 'White'); - break; - } + company.owner = owner; // Add employees. for (let j = 0; j < 20; j++) { - if (Math.random() < 0.2) { + if (j % 2 === 0) { const newPartTimeEmployee = new PartTimeEmployee( `firstname_${j}`, `lastname_${j}`, @@ -132,18 +127,30 @@ describe('polymorphism', () => { const json = TypedJSON.stringify(company, Company); const reparsed = TypedJSON.parse(json, Company); - if (log) { - console.log('Test: polymorphism...'); - console.log(company); - console.log(JSON.parse(json)); - console.log(reparsed); - console.log('Test finished.'); + const success = isEqual(company, reparsed); + + if (!success) { + console.log('Polymorphism test failed'); + console.log('company', company); + console.log('json', JSON.parse(json)); + console.log('reparsed', reparsed); } - return isEqual(company, reparsed); + return success; } it('should work', () => { - expect(test(false)).toBeTruthy(); + expect(test(new Employee('John', 'White', 240000, new Date(1992, 5, 27)))).toBeTruthy(); + expect(test(new Investor('John', 'White', 1700000))).toBeTruthy(); + const partTimeEmployee = new PartTimeEmployee( + 'John', + 'White', + 160000, + new Date(1992, 5, 27), + ); + + partTimeEmployee.workHours = 38; + expect(test(partTimeEmployee)).toBeTruthy(); + expect(test(new Person('John', 'White'))).toBeTruthy(); }); }); diff --git a/src/deserializer.ts b/src/deserializer.ts index 0ca4503..7fcadae 100644 --- a/src/deserializer.ts +++ b/src/deserializer.ts @@ -1,5 +1,5 @@ -import {identity, isSubtypeOf, isValueDefined, logError, nameof} from './helpers'; -import {JsonObjectMetadata, TypeResolver} from './metadata'; +import {identity, isValueDefined, logError, nameof} from './helpers'; +import {JsonObjectMetadata} from './metadata'; import {getOptionValue, mergeOptions, OptionsBase} from './options-base'; import { AnyT, @@ -12,19 +12,9 @@ import { } from './type-descriptor'; import {Constructor, IndexedObject, Serializable} from './types'; -export function defaultTypeResolver( - sourceObject: IndexedObject, - knownTypes: Map, -): Function | undefined { - if (sourceObject.__type != null) { - return knownTypes.get(sourceObject.__type); - } -} - export type DeserializerFn = ( sourceObject: Raw, typeDescriptor: TypeDescriptor, - knownTypes: Map, memberName: string, deserializer: Deserializer, memberOptions?: OptionsBase, @@ -37,8 +27,6 @@ export type DeserializerFn = ( export class Deserializer { options?: OptionsBase; - private typeResolver: TypeResolver = defaultTypeResolver; - private nameResolver?: (ctor: Function) => string; private errorHandler: (error: Error) => void = logError; private deserializationStrategy = new Map< Serializable, @@ -74,22 +62,6 @@ export class Deserializer { this.deserializationStrategy.set(type, deserializer); } - setNameResolver(nameResolverCallback: (ctor: Function) => string) { - this.nameResolver = nameResolverCallback; - } - - setTypeResolver(typeResolverCallback: TypeResolver) { - if (typeof typeResolverCallback as any !== 'function') { - throw new TypeError('\'typeResolverCallback\' is not a function.'); - } - - this.typeResolver = typeResolverCallback; - } - - getTypeResolver(): TypeResolver { - return this.typeResolver; - } - setErrorHandler(errorHandlerCallback: (error: Error) => void) { if (typeof errorHandlerCallback as any !== 'function') { throw new TypeError('\'errorHandlerCallback\' is not a function.'); @@ -105,7 +77,6 @@ export class Deserializer { convertSingleValue( sourceObject: any, typeDescriptor: TypeDescriptor, - knownTypes: Map, memberName = 'object', memberOptions?: OptionsBase, ): any { @@ -120,7 +91,6 @@ export class Deserializer { return deserializer( sourceObject, typeDescriptor, - knownTypes, memberName, this, memberOptions, @@ -128,7 +98,7 @@ export class Deserializer { } if (typeof sourceObject === 'object') { - return convertAsObject(sourceObject, typeDescriptor, knownTypes, memberName, this); + return convertAsObject(sourceObject, typeDescriptor, memberName, this); } let error = `Could not deserialize '${memberName}'; don't know how to deserialize type`; @@ -144,40 +114,6 @@ export class Deserializer { return new ctor(); } - mergeKnownTypes(...knownTypeMaps: Array>) { - const result = new Map(); - - knownTypeMaps.forEach(knownTypes => { - knownTypes.forEach((ctor, name) => { - if (this.nameResolver === undefined) { - result.set(name, ctor); - } else { - result.set(this.nameResolver(ctor), ctor); - } - }); - }); - - return result; - } - - createKnownTypesMap(knowTypes: Set) { - const map = new Map(); - - knowTypes.forEach(ctor => { - if (this.nameResolver === undefined) { - const knownTypeMeta = JsonObjectMetadata.getFromConstructor(ctor); - const customName = knownTypeMeta?.isExplicitlyMarked === true - ? knownTypeMeta.name - : null; - map.set(customName ?? ctor.name, ctor); - } else { - map.set(this.nameResolver(ctor), ctor); - } - }); - - return map; - } - retrievePreserveNull(memberOptions?: OptionsBase): boolean { return getOptionValue('preserveNull', mergeOptions(this.options, memberOptions)); } @@ -221,7 +157,6 @@ function srcTypeNameForDebug(sourceObject: any) { function deserializeDirectly( sourceObject: T, typeDescriptor: TypeDescriptor, - knownTypes: Map, objectName: string, ): T { if (sourceObject.constructor !== typeDescriptor.ctor) { @@ -237,7 +172,6 @@ function deserializeDirectly( function convertAsObject( sourceObject: IndexedObject, typeDescriptor: ConcreteTypeDescriptor, - knownTypes: Map, memberName: string, deserializer: Deserializer, ): IndexedObject | T | undefined { @@ -248,56 +182,21 @@ function convertAsObject( return undefined; } - let expectedSelfType = typeDescriptor.ctor; - let sourceObjectMetadata = JsonObjectMetadata.getFromConstructor(expectedSelfType); - let knownTypeConstructors = knownTypes; - let typeResolver = deserializer.getTypeResolver(); - - if (sourceObjectMetadata !== undefined) { - sourceObjectMetadata.processDeferredKnownTypes(); - - // Merge known types received from "above" with known types defined on the current type. - knownTypeConstructors = deserializer.mergeKnownTypes( - knownTypeConstructors, - deserializer.createKnownTypesMap(sourceObjectMetadata.knownTypes), - ); - if (sourceObjectMetadata.typeResolver != null) { - typeResolver = sourceObjectMetadata.typeResolver; - } - } - - // Check if a type-hint is available from the source object. - const typeFromTypeHint = typeResolver(sourceObject, knownTypeConstructors); - - if (typeFromTypeHint != null) { - // Check if type hint is a valid subtype of the expected source type. - if (isSubtypeOf(typeFromTypeHint, expectedSelfType)) { - // Hell yes. - expectedSelfType = typeFromTypeHint; - sourceObjectMetadata = JsonObjectMetadata.getFromConstructor(typeFromTypeHint); - - if (sourceObjectMetadata !== undefined) { - // Also merge new known types from subtype. - knownTypeConstructors = deserializer.mergeKnownTypes( - knownTypeConstructors, - deserializer.createKnownTypesMap(sourceObjectMetadata.knownTypes), - ); - } - } - } + const sourceObjectMetadata = JsonObjectMetadata.getSubTypeMetadata( + typeDescriptor.ctor, + sourceObject, + ); if (sourceObjectMetadata?.isExplicitlyMarked === true) { - const sourceMetadata = sourceObjectMetadata; // Strong-typed deserialization available, get to it. // First deserialize properties into a temporary object. const sourceObjectWithDeserializedProperties = {} as IndexedObject; - - const classOptions = mergeOptions(deserializer.options, sourceMetadata.options); + const classOptions = mergeOptions(deserializer.options, sourceObjectMetadata.options); // Deserialize by expected properties. - sourceMetadata.dataMembers.forEach((objMemberMetadata, propKey) => { + sourceObjectMetadata.dataMembers.forEach((objMemberMetadata, propKey) => { const objMemberValue = sourceObject[propKey]; - const objMemberDebugName = `${nameof(sourceMetadata.classType)}.${propKey}`; + const objMemberDebugName = `${nameof(sourceObjectMetadata.classType)}.${propKey}`; const objMemberOptions = mergeOptions(classOptions, objMemberMetadata.options); let revivedValue; @@ -312,7 +211,6 @@ function convertAsObject( revivedValue = deserializer.convertSingleValue( objMemberValue, objMemberMetadata.type(), - knownTypeConstructors, objMemberDebugName, objMemberOptions, ); @@ -362,7 +260,7 @@ function convertAsObject( return undefined; } } else { - targetObject = deserializer.instantiateType(expectedSelfType); + targetObject = deserializer.instantiateType(sourceObjectMetadata.classType); } // Finally, assign deserialized properties to target object. @@ -394,7 +292,6 @@ function convertAsObject( targetObject[sourceKey] = deserializer.convertSingleValue( sourceObject[sourceKey], new ConcreteTypeDescriptor(sourceObject[sourceKey].constructor), - knownTypes, sourceKey, ); }); @@ -406,7 +303,6 @@ function convertAsObject( function convertAsArray( sourceObject: any, typeDescriptor: TypeDescriptor, - knownTypes: Map, memberName: string, deserializer: Deserializer, memberOptions?: OptionsBase, @@ -442,7 +338,6 @@ function convertAsArray( return deserializer.convertSingleValue( element, typeDescriptor.elementType, - knownTypes, `${memberName}[${i}]`, memberOptions, ); @@ -459,7 +354,6 @@ function convertAsArray( function convertAsSet( sourceObject: any, typeDescriptor: TypeDescriptor, - knownTypes: Map, memberName: string, deserializer: Deserializer, memberOptions?: OptionsBase, @@ -496,7 +390,6 @@ function convertAsSet( resultSet.add(deserializer.convertSingleValue( element, typeDescriptor.elementType, - knownTypes, `${memberName}[${i}]`, memberOptions, )); @@ -518,7 +411,6 @@ function isExpectedMapShape(source: any, expectedShape: MapShape): boolean { function convertAsMap( sourceObject: any, typeDescriptor: TypeDescriptor, - knownTypes: Map, memberName: string, deserializer: Deserializer, memberOptions?: OptionsBase, @@ -562,7 +454,6 @@ function convertAsMap( const resultKey = deserializer.convertSingleValue( key, typeDescriptor.keyType, - knownTypes, keyMemberName, memberOptions, ); @@ -572,7 +463,6 @@ function convertAsMap( deserializer.convertSingleValue( sourceObject[key], typeDescriptor.valueType, - knownTypes, valueMemberName, memberOptions, ), @@ -590,7 +480,6 @@ function convertAsMap( const key = deserializer.convertSingleValue( element.key, typeDescriptor.keyType, - knownTypes, keyMemberName, memberOptions, ); @@ -602,7 +491,6 @@ function convertAsMap( deserializer.convertSingleValue( element.value, typeDescriptor.valueType, - knownTypes, valueMemberName, memberOptions, ), @@ -622,7 +510,6 @@ function convertAsMap( function deserializeDate( sourceObject: string | number | Date, typeDescriptor: TypeDescriptor, - knownTypes: Map, memberName: string, ): Date { // Support for Date with ISO 8601 format, or with numeric timestamp (milliseconds elapsed since @@ -656,7 +543,6 @@ function deserializeDate( function stringToArrayBuffer( sourceObject: string | number | Date, typeDescriptor: TypeDescriptor, - knownTypes: Map, memberName: string, ) { if (typeof sourceObject !== 'string') { @@ -673,7 +559,6 @@ function stringToArrayBuffer( function stringToDataView( sourceObject: string | number | Date, typeDescriptor: TypeDescriptor, - knownTypes: Map, memberName: string, ) { if (typeof sourceObject !== 'string') { @@ -701,7 +586,6 @@ function createArrayBufferFromString(input: string): ArrayBuffer { function convertAsFloatArray( sourceObject: string | number | Date, typeDescriptor: TypeDescriptor, - knownTypes: Map, memberName: string, ): T { const constructor = typeDescriptor.ctor as Constructor; @@ -720,7 +604,6 @@ function convertAsFloatArray( function convertAsUintArray( sourceObject: string | number | Date, typeDescriptor: TypeDescriptor, - knownTypes: Map, memberName: string, ): T { const constructor = typeDescriptor.ctor as Constructor; diff --git a/src/index.ts b/src/index.ts index a4111d1..3a6acae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,10 +2,8 @@ export { TypedJSON, ITypedJSONSettings, JsonTypes, - defaultTypeResolver, - defaultTypeEmitter, } from './parser'; -export {TypeResolver, TypeHintEmitter, JsonObjectMetadata} from './metadata'; +export {JsonObjectMetadata} from './metadata'; export { jsonObject, IJsonObjectOptions, @@ -13,6 +11,11 @@ export { IJsonObjectOptionsWithInitializer, IJsonObjectOptionsBase, } from './json-object'; +export { + discriminatorProperty, + jsonInheritance, + ObjectInheritanceOptions, +} from './json-inheritance'; export {jsonMember, IJsonMemberOptions} from './json-member'; export {jsonArrayMember, IJsonArrayMemberOptions} from './json-array-member'; export {jsonSetMember, IJsonSetMemberOptions} from './json-set-member'; diff --git a/src/json-inheritance.ts b/src/json-inheritance.ts new file mode 100644 index 0000000..a05258b --- /dev/null +++ b/src/json-inheritance.ts @@ -0,0 +1,96 @@ +import {JsonObjectMetadata} from './metadata'; +import {Serializable} from './types'; + +export interface ObjectInheritanceOptions { + + /** + * Function to be used to mutate the resulting serialization given the source object. + */ + onSerializeType?: (source: any, result: {[k: string]: any}) => {[k: string]: any}; + + /** + * Given the data to be parsed, return the matching subtype. + */ + resolveType: (data: any) => Serializable | undefined; +} + +export function jsonInheritance(options: ObjectInheritanceOptions) { + return (target: Serializable) => { + const objectMetadata = JsonObjectMetadata.ensurePresentInPrototype(target.prototype); + + objectMetadata.onSerializeType = options.onSerializeType; + objectMetadata.resolveType = options.resolveType; + }; +} + +export interface DiscriminatorPropertyOptions { + /** + * The name of the property containing the discriminator. + */ + property: string; + + /** + * An arrow function returning an object with as key the discriminator and value the type to + * instantiate. + */ + types: () => {[k: string]: Serializable}; +} + +/** + * Handle subtype lookup by matching the value of the given property to the types map. e.g. + * ``` + * discriminatorProperty({ + * property: 'type', + * types: () => ({ + * foo: Foo, + * bar: Bar, + * }), + * }) + * ``` + * with the following data: + * ``` + * { + * "type": "foo" + * } + * ``` + * This will result in TypedJSON looking up the value of `data.type`, here `foo`, in the map + * provided by `types`, resulting in an object of type `Foo`. When serializing `Foo`, the `type` + * property will be added with value `foo`. + */ +export function discriminatorProperty( + {property, types}: DiscriminatorPropertyOptions, +): ObjectInheritanceOptions { + let resolvedTypes: {[k: string]: Serializable} | undefined; + let reverseMapping: Map | undefined; + + const getResolvedTypes = () => { + if (resolvedTypes === undefined) { + resolvedTypes = types(); + } + + return resolvedTypes; + }; + + // Create a map for O(1) type-to-discriminator-lookup once. + const getReverseMapping = () => { + if (reverseMapping === undefined) { + const reverseMapItems: Array<[Function, string]> = Object.keys(getResolvedTypes()) + .map(discriminator => { + return [getResolvedTypes()[discriminator].prototype.constructor, discriminator]; + }); + reverseMapping = new Map(reverseMapItems); + } + + return reverseMapping; + }; + + return { + onSerializeType: (source, result) => { + result[property] = getReverseMapping().get(source.constructor); + return result; + }, + resolveType: data => { + return getResolvedTypes()[data[property]]; + }, + }; +} diff --git a/src/json-object.ts b/src/json-object.ts index 6f875d1..b6be64d 100644 --- a/src/json-object.ts +++ b/src/json-object.ts @@ -1,27 +1,10 @@ -import {JsonObjectMetadata, TypeHintEmitter, TypeResolver} from './metadata'; +import {JsonObjectMetadata} from './metadata'; import {extractOptionBase, OptionsBase} from './options-base'; import {Serializable} from './types'; export type InitializerCallback = (sourceObject: T, rawSourceObject: T) => T; export interface IJsonObjectOptionsBase extends OptionsBase { - /** - * An array of known types to recognize when encountering type-hints. - */ - knownTypes?: Array | null; - - /** - * A function that will emit a type hint on the resulting JSON. It will override the global - * typeEmitter. - */ - typeHintEmitter?: TypeHintEmitter | null; - - /** - * A function that given a source object will resolve the type that should be instantiated. - * It will override the global type resolver. - */ - typeResolver?: TypeResolver | null; - /** * The name of a static or instance method to call when deserialization * of the object is completed. @@ -105,13 +88,6 @@ export function jsonObject( objectMetadata.onDeserializedMethodName = options.onDeserialized; objectMetadata.beforeSerializationMethodName = options.beforeSerialization; - if (options.typeResolver != null) { - objectMetadata.typeResolver = options.typeResolver; - } - if (options.typeHintEmitter != null) { - objectMetadata.typeHintEmitter = options.typeHintEmitter; - } - // T extend Object so it is fine objectMetadata.initializerCallback = options.initializer as any; if (options.name != null) { @@ -121,12 +97,6 @@ export function jsonObject( if (optionsBase !== undefined) { objectMetadata.options = optionsBase; } - - if (options.knownTypes != null) { - options.knownTypes - .filter(knownType => Boolean(knownType)) - .forEach(knownType => objectMetadata.knownTypes.add(knownType)); - } } if (typeof optionsOrTarget === 'function') { diff --git a/src/metadata.ts b/src/metadata.ts index c9f1108..4317f88 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -5,18 +5,6 @@ import {IndexedObject, Serializable} from './types'; export const METADATA_FIELD_KEY = '__typedJsonJsonObjectMetadataInformation__'; -export type TypeResolver = ( - sourceObject: IndexedObject, - knownTypes: Map, -) => Function | undefined | null; -export type TypeHintEmitter - = ( - targetObject: IndexedObject, - sourceObject: IndexedObject, - expectedSourceType: Function, - sourceTypeMetadata?: JsonObjectMetadata, - ) => void; - export interface JsonMemberMetadata { /** If set, a default value will be emitted for uninitialized members. */ emitDefaultValue?: boolean | null; @@ -46,16 +34,6 @@ export class JsonObjectMetadata { dataMembers = new Map(); - /** Set of known types used for polymorphic deserialization */ - knownTypes = new Set>(); - - /** Known types to be evaluated when (de)serialization occurs */ - knownTypesDeferred: Array<() => TypeDescriptor> = []; - - /** If present override the global function */ - typeHintEmitter?: TypeHintEmitter | null; - /** If present override the global function */ - typeResolver?: TypeResolver | null; /** Gets or sets the constructor function for the jsonObject. */ classType: Function; @@ -78,10 +56,21 @@ export class JsonObjectMetadata { onDeserializedMethodName?: string | null; + /** + * Function to be used to mutate the resulting serialization given the source object. + */ + onSerializeType?: (source: any, result: {[k: string]: any}) => {[k: string]: any}; + beforeSerializationMethodName?: string | null; initializerCallback?: ((sourceObject: Object, rawSourceObject: Object) => Object) | null; + /** + * Given the data to be parsed, return the matching subtype. Used in conjunction with + * `subTypes`. + */ + resolveType?: (type: any) => Serializable | undefined; + constructor( classType: Function, ) { @@ -127,6 +116,33 @@ export class JsonObjectMetadata { } } + static getSubTypeMetadata( + ctor: Serializable, + data: IndexedObject, + ): JsonObjectMetadata | undefined { + const metadata = this.getFromConstructor(ctor); + + if (metadata === undefined) { + return undefined; + } + + if (metadata.resolveType === undefined) { + return metadata; + } + + const resolvedType = metadata.resolveType(data); + + if (resolvedType === ctor) { + return metadata; + } + + if (resolvedType === undefined) { + throw new Error(`No matching subtype returned for ${ctor.name}`); + } + + return this.getSubTypeMetadata(resolvedType, data); + } + static ensurePresentInPrototype(prototype: IndexedObject): JsonObjectMetadata { if (prototype.hasOwnProperty(METADATA_FIELD_KEY)) { return prototype[METADATA_FIELD_KEY]; @@ -137,14 +153,10 @@ export class JsonObjectMetadata { // Inherit json members and known types from parent @jsonObject (if any). const parentMetadata: JsonObjectMetadata | undefined = prototype[METADATA_FIELD_KEY]; if (parentMetadata !== undefined) { + objectMetadata.onSerializeType = parentMetadata.onSerializeType; parentMetadata.dataMembers.forEach((memberMetadata, propKey) => { objectMetadata.dataMembers.set(propKey, memberMetadata); }); - parentMetadata.knownTypes.forEach((knownType) => { - objectMetadata.knownTypes.add(knownType); - }); - objectMetadata.typeResolver = parentMetadata.typeResolver; - objectMetadata.typeHintEmitter = parentMetadata.typeHintEmitter; } Object.defineProperty(prototype, METADATA_FIELD_KEY, { @@ -156,26 +168,10 @@ export class JsonObjectMetadata { return objectMetadata; } - /** - * Gets the known type name of a jsonObject class for type hint. - * @param constructor The constructor class. - */ - static getKnownTypeNameFromType(constructor: Function): string { - const metadata = JsonObjectMetadata.getFromConstructor(constructor); - return metadata === undefined ? nameof(constructor) : nameof(metadata.classType); - } - private static doesHandleWithoutAnnotation(ctor: Function): boolean { return isDirectlySerializableNativeType(ctor) || isTypeTypedArray(ctor) || ctor === DataView || ctor === ArrayBuffer; } - - processDeferredKnownTypes(): void { - this.knownTypesDeferred.forEach(typeThunk => { - typeThunk().getTypes().forEach(ctor => this.knownTypes.add(ctor)); - }); - this.knownTypesDeferred = []; - } } export function injectMetadataInformation( @@ -216,11 +212,6 @@ export function injectMetadataInformation( // with '@jsonObject' as well. const objectMetadata = JsonObjectMetadata.ensurePresentInPrototype(prototype); - if (metadata.deserializer === undefined) { - // If deserializer is not present then type must be - objectMetadata.knownTypesDeferred.push(metadata.type!); - } - // clear metadata of undefined properties to save memory (Object.keys(metadata) as Array) .forEach((key) => (metadata[key] === undefined) && delete metadata[key]); diff --git a/src/parser.ts b/src/parser.ts index 010c2cd..381db7a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,14 +1,13 @@ -import {defaultTypeResolver, Deserializer} from './deserializer'; -import {logError, logWarning, nameof, parseToJSObject} from './helpers'; +import {Deserializer} from './deserializer'; +import {logError, nameof, parseToJSObject} from './helpers'; import {createArrayType} from './json-array-member'; -import {JsonObjectMetadata, TypeHintEmitter, TypeResolver} from './metadata'; +import {JsonObjectMetadata} from './metadata'; import {extractOptionBase, OptionsBase} from './options-base'; -import {defaultTypeEmitter, Serializer} from './serializer'; +import {Serializer} from './serializer'; import {ensureTypeDescriptor, MapT, SetT} from './type-descriptor'; -import {Constructor, IndexedObject, Serializable} from './types'; +import {IndexedObject, Serializable} from './types'; export type JsonTypes = Object | boolean | string | number | null | undefined; -export {defaultTypeResolver, defaultTypeEmitter}; export interface MappedTypeConverters { @@ -37,24 +36,6 @@ export interface ITypedJSONSettings extends OptionsBase { */ mappedTypes?: Map, MappedTypeConverters> | null; - /** - * Sets a callback that determines the constructor of the correct sub-type of polymorphic - * objects while deserializing. - * The default behavior is to read the type-name from the '__type' property of 'sourceObject', - * and look it up in 'knownTypes'. - * The constructor of the sub-type should be returned. - */ - typeResolver?: TypeResolver | null; - - nameResolver?: ((ctor: Function) => string) | null; - - /** - * Sets a callback that writes type-hints to serialized objects. - * The default behavior is to write the type-name to the '__type' property, if a derived type - * is present in place of a base type. - */ - typeHintEmitter?: TypeHintEmitter | null; - /** * Sets the amount of indentation to use in produced JSON strings. * Default value is 0, or no indentation. @@ -62,8 +43,6 @@ export interface ITypedJSONSettings extends OptionsBase { indent?: number | null; replacer?: ((key: string, value: any) => any) | null; - - knownTypes?: Array> | null; } export class TypedJSON { @@ -72,11 +51,9 @@ export class TypedJSON { private serializer: Serializer = new Serializer(); private deserializer: Deserializer = new Deserializer(); - private globalKnownTypes: Array> = []; private indent: number = 0; private rootConstructor: Serializable; private errorHandler: (e: Error) => void; - private nameResolver: (ctor: Function) => string; private replacer?: (key: string, value: any) => any; /** @@ -95,7 +72,6 @@ export class TypedJSON { ); } - this.nameResolver = (ctor) => nameof(ctor); this.rootConstructor = rootConstructor; this.errorHandler = (error) => logError(error); @@ -329,14 +305,6 @@ export class TypedJSON { ...settings, }; - if (settings.knownTypes != null - && TypedJSON._globalConfig.knownTypes != null) { - // Merge known-types (also de-duplicate them, so Array -> Set -> Array). - settings.knownTypes = Array.from(new Set( - settings.knownTypes.concat(TypedJSON._globalConfig.knownTypes), - )); - } - const options = extractOptionBase(settings); this.serializer.options = options; this.deserializer.options = options; @@ -350,12 +318,7 @@ export class TypedJSON { if (settings.replacer != null) { this.replacer = settings.replacer; } - if (settings.typeResolver != null) { - this.deserializer.setTypeResolver(settings.typeResolver); - } - if (settings.typeHintEmitter != null) { - this.serializer.setTypeHintEmitter(settings.typeHintEmitter); - } + if (settings.indent != null) { this.indent = settings.indent; } @@ -365,25 +328,6 @@ export class TypedJSON { this.setSerializationStrategies(type, upDown); }); } - - if (settings.nameResolver != null) { - this.nameResolver = settings.nameResolver; - this.deserializer.setNameResolver(settings.nameResolver); - } - - if (settings.knownTypes != null) { - // Type-check knownTypes elements to recognize errors in advance. - settings.knownTypes.forEach((knownType: any, i) => { - if (typeof knownType === 'undefined' || knownType === null) { - logWarning( - `TypedJSON.config: 'knownTypes' contains an undefined/null value` - + ` (element ${i}).`, - ); - } - }); - - this.globalKnownTypes = settings.knownTypes; - } } mapType(type: Serializable, converters: MappedTypeConverters): void { @@ -399,26 +343,12 @@ export class TypedJSON { parse(object: any): T | undefined { const json = parseToJSObject(object, this.rootConstructor); - const rootMetadata = JsonObjectMetadata.getFromConstructor(this.rootConstructor); let result: T | undefined; - const knownTypes = new Map(); - - this.globalKnownTypes.filter(ktc => ktc).forEach(knownTypeCtor => { - knownTypes.set(this.nameResolver(knownTypeCtor), knownTypeCtor); - }); - - if (rootMetadata !== undefined) { - rootMetadata.processDeferredKnownTypes(); - rootMetadata.knownTypes.forEach(knownTypeCtor => { - knownTypes.set(this.nameResolver(knownTypeCtor), knownTypeCtor); - }); - } try { result = this.deserializer.convertSingleValue( json, ensureTypeDescriptor(this.rootConstructor), - knownTypes, ) as T; } catch (e) { this.errorHandler(e); @@ -438,7 +368,6 @@ export class TypedJSON { return this.deserializer.convertSingleValue( json, createArrayType(ensureTypeDescriptor(this.rootConstructor), dimensions), - this._mapKnownTypes(this.globalKnownTypes), ); } @@ -447,7 +376,6 @@ export class TypedJSON { return this.deserializer.convertSingleValue( json, SetT(this.rootConstructor), - this._mapKnownTypes(this.globalKnownTypes), ); } @@ -456,7 +384,6 @@ export class TypedJSON { return this.deserializer.convertSingleValue( json, MapT(keyConstructor, this.rootConstructor), - this._mapKnownTypes(this.globalKnownTypes), ); } @@ -552,14 +479,6 @@ export class TypedJSON { return JSON.stringify(this.toPlainMap(object, keyConstructor), this.replacer, this.indent); } - private _mapKnownTypes(constructors: Array>) { - const map = new Map>(); - - constructors.filter(ctor => ctor).forEach(ctor => map.set(this.nameResolver(ctor), ctor)); - - return map; - } - private setSerializationStrategies( type: Serializable, converters: MappedTypeConverters, diff --git a/src/serializer.ts b/src/serializer.ts index ff8ca5b..a8f870e 100644 --- a/src/serializer.ts +++ b/src/serializer.ts @@ -5,7 +5,7 @@ import { logError, nameof, } from './helpers'; -import {JsonObjectMetadata, TypeHintEmitter} from './metadata'; +import {JsonObjectMetadata} from './metadata'; import {getOptionValue, mergeOptions, OptionsBase} from './options-base'; import { AnyT, @@ -18,21 +18,6 @@ import { } from './type-descriptor'; import {IndexedObject, Serializable} from './types'; -export function defaultTypeEmitter( - targetObject: IndexedObject, - sourceObject: IndexedObject, - expectedSourceType: Function, - sourceTypeMetadata?: JsonObjectMetadata, -) { - // By default, we put a "__type" property on the output object if the actual object is not the - // same as the expected one, so that deserialization will know what to deserialize into (given - // the required known-types are defined, and the object is a valid subtype of the expected - // type). - if (sourceObject.constructor !== expectedSourceType) { - targetObject.__type = sourceTypeMetadata?.name ?? nameof(sourceObject.constructor); - } -} - /** * @param sourceObject The original object that should be serialized. * @param typeDescriptor Instance of TypeDescriptor containing information about expected @@ -61,7 +46,6 @@ export type SerializerFn = ( */ export class Serializer { options?: OptionsBase; - private typeHintEmitter: TypeHintEmitter = defaultTypeEmitter; private errorHandler: (error: Error) => void = logError; private serializationStrategy = new Map< Serializable, @@ -100,18 +84,6 @@ export class Serializer { this.serializationStrategy.set(type, serializer); } - setTypeHintEmitter(typeEmitterCallback: TypeHintEmitter) { - if (typeof typeEmitterCallback as any !== 'function') { - throw new TypeError('\'typeEmitterCallback\' is not a function.'); - } - - this.typeHintEmitter = typeEmitterCallback; - } - - getTypeHintEmitter(): TypeHintEmitter { - return this.typeHintEmitter; - } - setErrorHandler(errorHandlerCallback: (error: Error) => void) { if (typeof errorHandlerCallback as any !== 'function') { throw new TypeError('\'errorHandlerCallback\' is not a function.'); @@ -188,7 +160,6 @@ function convertAsObject( ) { let sourceTypeMetadata: JsonObjectMetadata | undefined; let targetObject: IndexedObject; - let typeHintEmitter = serializer.getTypeHintEmitter(); if (sourceObject.constructor !== typeDescriptor.ctor && sourceObject instanceof typeDescriptor.ctor) { @@ -233,9 +204,6 @@ function convertAsObject( targetObject = {}; const classOptions = mergeOptions(serializer.options, sourceMeta.options); - if (sourceMeta.typeHintEmitter != null) { - typeHintEmitter = sourceMeta.typeHintEmitter; - } sourceMeta.dataMembers.forEach((objMemberMetadata) => { const objMemberOptions = mergeOptions(classOptions, objMemberMetadata.options); @@ -262,10 +230,9 @@ function convertAsObject( targetObject[objMemberMetadata.name] = serialized; } }); - } - // Add type-hint. - typeHintEmitter(targetObject, sourceObject, typeDescriptor.ctor, sourceTypeMetadata); + targetObject = sourceMeta.onSerializeType?.(sourceObject, targetObject) ?? targetObject; + } return targetObject; }