Skip to content

Commit

Permalink
fix(type): add executeTypeArgumentAsArray + custom iterable example w…
Browse files Browse the repository at this point in the history
…ith manual implementation
  • Loading branch information
marcj committed Oct 30, 2024
1 parent 2be4ce6 commit 0781a1a
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 38 deletions.
25 changes: 15 additions & 10 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
28 changes: 21 additions & 7 deletions packages/type/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down Expand Up @@ -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)};
}
`);
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/type/tests/serializer-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,7 +124,7 @@ test('pointer example', () => {
expect(point.y).toBe(2);

{
expect(() => deserialize<Point>(['vbb'])).toThrowError(SerializationError);
expect(() => deserialize<Point>(['vbb'])).toThrowError(ValidationError);
expect(() => deserialize<Point>(['vbb'])).toThrow('Expected array with two elements')
}

Expand Down
117 changes: 97 additions & 20 deletions packages/type/tests/use-cases.spec.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,124 @@
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<T> implements Iterable<T> {
items: T[] = [];
class MyIterable<T> implements Iterable<T> {
items: T[] = [];

constructor(items: T[] = []) {
this.items = items;
}
constructor(items: T[] = []) {
this.items = items;
}

[Symbol.iterator](): Iterator<T> {
return this.items[Symbol.iterator]();
}
[Symbol.iterator](): Iterator<T> {
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<string>;
type T2 = MyIterable<number>;

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);
});
});

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<unknown>) => value.items);
// see explanation in deserializeRegistry
forwardSetToArray(type, state);
executeTypeArgumentAsArray(type, 0, state);
});

const a = deserialize<T1>(['a', 'b']);
const b = deserialize<T1>(['a', 2]);
const c = deserialize<T1>('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<string>();
obj1.add('a');
obj1.add('b');

const json1 = serialize<T1>(obj1);
console.log(json1);
expect(json1).toEqual(['a', 'b']);

const back1 = deserialize<T1>(json1);
console.log(back1);
expect(back1).toBeInstanceOf(MyIterable);
expect(back1.items).toEqual(['a', 'b']);

const errors = validate<T1>(back1);
expect(errors).toEqual([]);

const back2 = deserialize<T2>([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<string>;
type T2 = MyIterable<number>;

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<unknown>) => {
return value.items.map((v: unknown) => itemSerializer(v));
});
});

expect(deserialize<T1>(['a', 'b'])).toBeInstanceOf(MyIterable);
expect(deserialize<T1>(['a', 2])).toBeInstanceOf(MyIterable);
expect(() => deserialize<T1>('abc')).toThrow('Expected array');

const obj1 = new MyIterable<string>();
obj1.add('a');
obj1.add('b');
Expand Down

0 comments on commit 0781a1a

Please sign in to comment.