Skip to content

Commit

Permalink
feat(type): new TemplateState.touch and isTypeClassOf
Browse files Browse the repository at this point in the history
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();
        });
    }
);
```
  • Loading branch information
marcj committed May 4, 2024
1 parent 96dd2d8 commit cdf3272
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 2 deletions.
14 changes: 14 additions & 0 deletions packages/type/src/reflection/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
20 changes: 20 additions & 0 deletions packages/type/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
30 changes: 29 additions & 1 deletion packages/type/tests/class.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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<Base>())).toBe(true);
expect(isTypeClassOf(Base)(typeOf<Base2>())).toBe(true);

expect(isTypeClassOf(Base2)(typeOf<Base2>())).toBe(true);
expect(isTypeClassOf(Base2)(typeOf<Base>())).toBe(false);

expect(isTypeClassOf(Base)(typeOf<Derived>())).toBe(true);
expect(isTypeClassOf(Base)(typeOf<Derived2>())).toBe(true);

expect(isTypeClassOf(Base2)(typeOf<Derived>())).toBe(false);
expect(isTypeClassOf(Base2)(typeOf<Derived2>())).toBe(true);
});
84 changes: 83 additions & 1 deletion packages/type/tests/serializer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
Group,
int8,
integer,
isCustomTypeClass,
isTypeClassOf,
MapName,
metaAnnotation,
PrimaryKey,
Expand All @@ -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';
Expand Down Expand Up @@ -1039,6 +1041,86 @@ test('enum mixed case', () => {
expect(cast<number | Units>('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<Target>({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<Target>({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<Target>({id: 1}, undefined, serializer);
expect(target.loaded).toBe(true);
});

test('enum union', () => {
enum StatEnginePowerUnit {
Hp = 'hp',
Expand Down

0 comments on commit cdf3272

Please sign in to comment.