From 0781a1ab733a5b06b0c89ec854274e0a8115696a Mon Sep 17 00:00:00 2001 From: "Marc J. Schmidt" Date: Wed, 30 Oct 2024 17:31:49 +0100 Subject: [PATCH] fix(type): add executeTypeArgumentAsArray + custom iterable example with manual implementation --- .github/workflows/main.yml | 25 +++-- packages/type/src/serializer.ts | 28 +++-- packages/type/tests/serializer-api.spec.ts | 3 +- packages/type/tests/use-cases.spec.ts | 117 +++++++++++++++++---- 4 files changed, 135 insertions(+), 38 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 667640fb7..856719fc3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -77,8 +77,9 @@ jobs: packages/stopwatch/ \ packages/workflow/ \ packages/type/ - - name: Send coverage - run: ./node_modules/.bin/codecov -f coverage/*.json +# this is broken at the moment +# - name: Send coverage +# run: ./node_modules/.bin/codecov -f coverage/*.json orm-postgres: needs: @@ -111,8 +112,9 @@ jobs: - name: Test run: npm run test:coverage packages/postgres/ - - name: Send coverage - run: ./node_modules/.bin/codecov -f coverage/*.json +# this is broken at the moment +# - name: Send coverage +# run: ./node_modules/.bin/codecov -f coverage/*.json orm-mysql: needs: @@ -144,8 +146,9 @@ jobs: - name: Test run: npm run test:coverage packages/mysql/ - - name: Send coverage - run: ./node_modules/.bin/codecov -f coverage/*.json +# this is broken at the moment +# - name: Send coverage +# run: ./node_modules/.bin/codecov -f coverage/*.json orm-sqlite: needs: @@ -165,8 +168,9 @@ jobs: - name: Test run: npm run test:coverage packages/sqlite/ - - name: Send coverage - run: ./node_modules/.bin/codecov -f coverage/*.json +# this is broken at the moment +# - name: Send coverage +# run: ./node_modules/.bin/codecov -f coverage/*.json orm-mongo: needs: @@ -207,5 +211,6 @@ jobs: - name: Test run: npm run test:coverage packages/mongo/ - - name: Send coverage - run: ./node_modules/.bin/codecov -f coverage/*.json +# this is broken at the moment +# - name: Send coverage +# run: ./node_modules/.bin/codecov -f coverage/*.json diff --git a/packages/type/src/serializer.ts b/packages/type/src/serializer.ts index 0abcd577f..05f04b4db 100644 --- a/packages/type/src/serializer.ts +++ b/packages/type/src/serializer.ts @@ -220,10 +220,10 @@ export function getSerializeFunction(type: Type, registry: TemplateRegistry, nam return jit[id]; } -export function createSerializeFunction(type: Type, registry: TemplateRegistry, namingStrategy: NamingStrategy = new NamingStrategy(), path: string = '', jitStack = new JitStack()): SerializeFunction { +export function createSerializeFunction(type: Type, registry: TemplateRegistry, namingStrategy: NamingStrategy = new NamingStrategy(), path: string | RuntimeCode | (string | RuntimeCode)[] = '', jitStack = new JitStack()): SerializeFunction { const compiler = new CompilerContext(); - const state = new TemplateState('result', 'data', compiler, registry, namingStrategy, jitStack, path ? [path] : []); + const state = new TemplateState('result', 'data', compiler, registry, namingStrategy, jitStack, isArray(path) ? path : path ? [path] : []); if (state.registry === state.registry.serializer.deserializeRegistry) { state.target = 'deserialize'; } @@ -546,7 +546,7 @@ export class TemplateState { if (error instanceof SerializationError) { error.path = ${collapsePath(this.path)} + (error.path ? '.' + error.path : ''); } - throw error; + ${this.throwCode('any', 'error.message', this.accessor)}; } `); } @@ -1643,11 +1643,9 @@ export function getSetTypeToArray(type: TypeClass): TypeArray { const value = type.arguments?.[0] || { kind: ReflectionKind.any }; - jit.forwardSetToArray = { + return jit.forwardSetToArray = { kind: ReflectionKind.array, type: value, - }; - - return jit.forwardSetToArray; + } as TypeArray; } export function getMapTypeToArray(type: TypeClass): TypeArray { @@ -1669,6 +1667,22 @@ export function getMapTypeToArray(type: TypeClass): TypeArray { return jit.forwardMapToArray; } +export function getNTypeToArray(type: TypeClass, n: number): TypeArray { + const jit = getTypeJitContainer(type); + const name = `forwardNTypeToArray${n}`; + if (jit[name]) return jit[name]; + + const value = type.arguments?.[n] || { kind: ReflectionKind.any }; + + return jit[name] = { + kind: ReflectionKind.array, type: value, + } as TypeArray; +} + +export function executeTypeArgumentAsArray(type: TypeClass, typeIndex: number, state: TemplateState) { + executeTemplates(state, getNTypeToArray(type, typeIndex), true, false); +} + export function forwardSetToArray(type: TypeClass, state: TemplateState) { executeTemplates(state, getSetTypeToArray(type), true, false); } diff --git a/packages/type/tests/serializer-api.spec.ts b/packages/type/tests/serializer-api.spec.ts index 24bb06acb..462808ac8 100644 --- a/packages/type/tests/serializer-api.spec.ts +++ b/packages/type/tests/serializer-api.spec.ts @@ -3,6 +3,7 @@ import { EmptySerializer, executeTemplates, SerializationError, serializer, Seri import { ReflectionKind, stringifyResolvedType } from '../src/reflection/type.js'; import { CompilerContext } from '@deepkit/core'; import { cast, deserialize, serialize } from '../src/serializer-facade.js'; +import { ValidationError } from '../src/validator'; test('remove guard for string', () => { //if the original value (before convert to string) is null, it should stay null @@ -123,7 +124,7 @@ test('pointer example', () => { expect(point.y).toBe(2); { - expect(() => deserialize(['vbb'])).toThrowError(SerializationError); + expect(() => deserialize(['vbb'])).toThrowError(ValidationError); expect(() => deserialize(['vbb'])).toThrow('Expected array with two elements') } diff --git a/packages/type/tests/use-cases.spec.ts b/packages/type/tests/use-cases.spec.ts index 752afd04a..34121f159 100644 --- a/packages/type/tests/use-cases.spec.ts +++ b/packages/type/tests/use-cases.spec.ts @@ -1,34 +1,37 @@ import { expect, test } from '@jest/globals'; -import { forwardSetToArray, serializer } from '../src/serializer'; +import { createSerializeFunction, executeTypeArgumentAsArray, SerializeFunction, serializer, TemplateState } from '../src/serializer'; import { deserialize, serialize } from '../src/serializer-facade'; import { validate } from '@deepkit/type'; +import { TypeClass } from '../src/reflection/type'; -test('custom iterable', () => { - class MyIterable implements Iterable { - items: T[] = []; +class MyIterable implements Iterable { + items: T[] = []; - constructor(items: T[] = []) { - this.items = items; - } + constructor(items: T[] = []) { + this.items = items; + } - [Symbol.iterator](): Iterator { - return this.items[Symbol.iterator](); - } + [Symbol.iterator](): Iterator { + return this.items[Symbol.iterator](); + } - add(item: T) { - this.items.push(item); - } + add(item: T) { + this.items.push(item); } +} +/** + * This example shows how to use `executeTypeArgumentAsArray` to automatically convert a + * array-like custom type easily. + */ +test('custom iterable', () => { type T1 = MyIterable; type T2 = MyIterable; serializer.deserializeRegistry.registerClass(MyIterable, (type, state) => { - // takes first argument and deserializes as array, just like Set. - // works because first template argument defined the iterable type. - // can not be used if the iterable type is not known or not the first template argument. - forwardSetToArray(type, state); - // at this point `value` contains the value of `forwardSetToArray`, which is T as array. + // takes first argument (0) and deserializes as array. + executeTypeArgumentAsArray(type, 0, state); + // at this point current value contains the value of `executeTypeArgumentAsArray`, which is T as array. // we forward this value to OrderedSet constructor. state.convert(value => { return new MyIterable(value); @@ -36,12 +39,86 @@ test('custom iterable', () => { }); serializer.serializeRegistry.registerClass(MyIterable, (type, state) => { - // Set `MyIterable.items` as current value, so that forwardSetToArray operates on it. + // set `MyIterable.items` as current value, so that executeTypeArgumentAsArray operates on it. state.convert((value: MyIterable) => value.items); // see explanation in deserializeRegistry - forwardSetToArray(type, state); + executeTypeArgumentAsArray(type, 0, state); + }); + + const a = deserialize(['a', 'b']); + const b = deserialize(['a', 2]); + const c = deserialize('abc'); + expect(a).toBeInstanceOf(MyIterable); + expect(a.items).toEqual(['a', 'b']); + expect(b).toBeInstanceOf(MyIterable); + expect(b.items).toEqual(['a', '2']); + expect(c).toBeInstanceOf(MyIterable); + expect(c.items).toEqual([]); + + const obj1 = new MyIterable(); + obj1.add('a'); + obj1.add('b'); + + const json1 = serialize(obj1); + console.log(json1); + expect(json1).toEqual(['a', 'b']); + + const back1 = deserialize(json1); + console.log(back1); + expect(back1).toBeInstanceOf(MyIterable); + expect(back1.items).toEqual(['a', 'b']); + + const errors = validate(back1); + expect(errors).toEqual([]); + + const back2 = deserialize([1, '2']); + console.log(back2); + expect(back2).toBeInstanceOf(MyIterable); + expect(back2.items).toEqual([1, 2]); +}); + +/** + * This example shows how to manually implement a custom iterable using state.convert(). + */ +test('custom iterable manual', () => { + type T1 = MyIterable; + type T2 = MyIterable; + + function getFirstArgumentSerializer(type: TypeClass, state: TemplateState): SerializeFunction { + const firstArgument = type.arguments?.[0]; + if (!firstArgument) throw new Error('First type argument in MyIterable is missing'); + return createSerializeFunction(firstArgument, state.registry, state.namingStrategy, state.path); + } + + serializer.deserializeRegistry.registerClass(MyIterable, (type, state) => { + const itemSerializer = getFirstArgumentSerializer(type, state); + + state.convert((value: any) => { + // convert() in `deserializeRegistry` accepts `any`, so we have to check if it's an array. + // you can choose to throw or silently ignore invalid values, + // by returning empty `return new MyIterable([]);` + if (!Array.isArray(value)) throw new Error('Expected array'); + + // convert each item in the array to the correct type. + const items = value.map((v: unknown) => itemSerializer(v)); + return new MyIterable(items); + }); }); + serializer.serializeRegistry.registerClass(MyIterable, (type, state) => { + const itemSerializer = getFirstArgumentSerializer(type, state); + + // convert() in `serializeRegistry` gets the actual runtime type, + // as anything else would be a TypeScript type error. + state.convert((value: MyIterable) => { + return value.items.map((v: unknown) => itemSerializer(v)); + }); + }); + + expect(deserialize(['a', 'b'])).toBeInstanceOf(MyIterable); + expect(deserialize(['a', 2])).toBeInstanceOf(MyIterable); + expect(() => deserialize('abc')).toThrow('Expected array'); + const obj1 = new MyIterable(); obj1.add('a'); obj1.add('b');