From cdf3272e05f63487c9e0da7776aa232ba1fe88b5 Mon Sep 17 00:00:00 2001 From: "Marc J. Schmidt" Date: Sat, 4 May 2024 23:46:19 +0200 Subject: [PATCH] feat(type): new TemplateState.touch and isTypeClassOf This allows to call for example methods on the last known value in the template execution chain. For example calling 'onLoad' on class instances. ```typescript deserializeRegistry.addDecorator( isCustomTypeClass, (type, state) => { state.touch((value) => { if ('onLoad' in value) value.onLoad(); }); } ); ``` --- packages/type/src/reflection/type.ts | 14 +++++ packages/type/src/serializer.ts | 20 ++++++ packages/type/tests/class.spec.ts | 30 ++++++++- packages/type/tests/serializer.spec.ts | 84 +++++++++++++++++++++++++- 4 files changed, 146 insertions(+), 2 deletions(-) diff --git a/packages/type/src/reflection/type.ts b/packages/type/src/reflection/type.ts index b056ebe0b..7f8dacb16 100644 --- a/packages/type/src/reflection/type.ts +++ b/packages/type/src/reflection/type.ts @@ -2296,6 +2296,20 @@ export function isCustomTypeClass(type: Type): type is TypeClass { return type.kind === ReflectionKind.class && !isGlobalTypeClass(type); } +/** + * Returns a type predicate that checks if the given type is a class and is of the given classType. + * If withInheritance is true, it also checks if the type is a subclass of the given classType. + */ +export function isTypeClassOf(classType: ClassType, withInheritance: boolean = true): (type: Type) => boolean { + if (!withInheritance) return (type: Type) => type.kind === ReflectionKind.class && type.classType === classType; + + return (type: Type) => { + if (type.kind !== ReflectionKind.class) return false; + const chain = getInheritanceChain(type.classType); + return chain.includes(classType); + }; +} + /** * Returns the members of a class or object literal. */ diff --git a/packages/type/src/serializer.ts b/packages/type/src/serializer.ts index 446960f87..6feffca3f 100644 --- a/packages/type/src/serializer.ts +++ b/packages/type/src/serializer.ts @@ -531,6 +531,26 @@ export class TemplateState { this.addSetter(`${converter}(${this.accessor})`); } + /** + * Allows to add a custom code that is executed on the current `this.accessor` value. + * + * @example + * ```typescript + * serializer.deserializeRegistry.addDecorator( + * isCustomTypeClass, + * (type, state) => { + * state.touch((value) => { + * if ('onLoad' in value) value.onLoad(); + * }); + * } + * ); + * ``` + */ + touch(callback: (value: any) => void) { + const touch = this.setVariable('touch', callback); + this.addCode(`${touch}(${this.setter});`); + } + /** * Stop executing next templates. */ diff --git a/packages/type/tests/class.spec.ts b/packages/type/tests/class.spec.ts index 53cbd2105..983eeafaa 100644 --- a/packages/type/tests/class.spec.ts +++ b/packages/type/tests/class.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from '@jest/globals'; -import { isCustomTypeClass, isGlobalTypeClass, stringifyResolvedType, stringifyType } from '../src/reflection/type.js'; +import { isCustomTypeClass, isGlobalTypeClass, isTypeClassOf, stringifyResolvedType, stringifyType } from '../src/reflection/type.js'; import { ReflectionClass, typeOf } from '../src/reflection/reflection.js'; test('index access inheritance', () => { @@ -79,3 +79,31 @@ test('isGlobalTypeClass', () => { expect(isGlobalTypeClass(reflection.getProperty('created').type)).toBe(true); expect(isCustomTypeClass(reflection.getProperty('created').type)).toBe(false); }); + +test('isTypeClassOf', () => { + class Base { + + } + class Base2 extends Base { + + } + + class Derived extends Base { + + } + class Derived2 extends Base2 { + + } + + expect(isTypeClassOf(Base)(typeOf())).toBe(true); + expect(isTypeClassOf(Base)(typeOf())).toBe(true); + + expect(isTypeClassOf(Base2)(typeOf())).toBe(true); + expect(isTypeClassOf(Base2)(typeOf())).toBe(false); + + expect(isTypeClassOf(Base)(typeOf())).toBe(true); + expect(isTypeClassOf(Base)(typeOf())).toBe(true); + + expect(isTypeClassOf(Base2)(typeOf())).toBe(false); + expect(isTypeClassOf(Base2)(typeOf())).toBe(true); +}); diff --git a/packages/type/tests/serializer.spec.ts b/packages/type/tests/serializer.spec.ts index f6795b996..179c822fd 100644 --- a/packages/type/tests/serializer.spec.ts +++ b/packages/type/tests/serializer.spec.ts @@ -19,6 +19,8 @@ import { Group, int8, integer, + isCustomTypeClass, + isTypeClassOf, MapName, metaAnnotation, PrimaryKey, @@ -29,7 +31,7 @@ import { TypeProperty, TypePropertySignature, } from '../src/reflection/type.js'; -import { createSerializeFunction, getSerializeFunction, NamingStrategy, serializer, underscoreNamingStrategy } from '../src/serializer.js'; +import { createSerializeFunction, getSerializeFunction, NamingStrategy, Serializer, serializer, underscoreNamingStrategy } from '../src/serializer.js'; import { cast, deserialize, patch, serialize } from '../src/serializer-facade.js'; import { getClassName } from '@deepkit/core'; import { entity, t } from '../src/decorator.js'; @@ -1039,6 +1041,86 @@ test('enum mixed case', () => { expect(cast('Gram')).toBe(Units.GRAM); }); +test('onLoad call', () => { + class Target { + id: number = 0; + loaded = false; + + onLoad(): void { + this.loaded = true; + } + } + + const serializer = new class extends Serializer { + override registerSerializers() { + super.registerSerializers(); + this.deserializeRegistry.addDecorator( + (type: Type) => type.kind === ReflectionKind.class && type.classType === Target, + (type, state) => { + state.addCode(`${state.setter}.onLoad();`); + } + ) + } + } + + const target = cast({id: 1}, undefined, serializer); + expect(target.loaded).toBe(true); +}); + +test('onLoad call2', () => { + class Target { + id: number = 0; + loaded = false; + + onLoad(): void { + this.loaded = true; + } + } + + const serializer = new class extends Serializer { + override registerSerializers() { + super.registerSerializers(); + this.deserializeRegistry.addDecorator( + isTypeClassOf(Target), + (type, state) => { + state.touch((target: Target) => target.onLoad()) + } + ) + } + } + + const target = cast({id: 1}, undefined, serializer); + expect(target.loaded).toBe(true); +}); + +test('onLoad call3', () => { + class Target { + id: number = 0; + loaded = false; + + onLoad(): void { + this.loaded = true; + } + } + + const serializer = new class extends Serializer { + override registerSerializers() { + super.registerSerializers(); + this.deserializeRegistry.addDecorator( + isCustomTypeClass, + (type, state) => { + state.touch((value) => { + if ('onLoad' in value) value.onLoad(); + }); + } + ); + } + } + + const target = cast({id: 1}, undefined, serializer); + expect(target.loaded).toBe(true); +}); + test('enum union', () => { enum StatEnginePowerUnit { Hp = 'hp',