Skip to content

Commit

Permalink
feat(type): automatically assign .path to SerializationError in Templ…
Browse files Browse the repository at this point in the history
…ateState.convert() errors
  • Loading branch information
marcj committed May 29, 2024
1 parent 7b19397 commit 23781a1
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 5 deletions.
2 changes: 1 addition & 1 deletion packages/bson/src/bson-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class BaseParser {
case BSONType.ARRAY:
return parseArray(this);
default:
throw new SerializationError('Unsupported BSON type ' + elementType, '');
throw new SerializationError('Unsupported BSON type ' + elementType);
}
}

Expand Down
26 changes: 23 additions & 3 deletions packages/type/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ export function createSerializeFunction(type: Type, registry: TemplateRegistry,
compiler.context.set('typeSettings', typeSettings);
compiler.context.set('UnpopulatedCheck', UnpopulatedCheck);
compiler.context.set('UnpopulatedCheckReturnSymbol', UnpopulatedCheck.ReturnSymbol);
compiler.context.set('SerializationError', SerializationError);
compiler.context.set('ValidationErrorItem', ValidationErrorItem);

const code = `
var result;
Expand Down Expand Up @@ -285,7 +287,7 @@ export function createTypeGuardFunction(type: Type, stateIn?: Partial<TemplateSt
}

export class SerializationError extends CustomError {
constructor(public originalMessage: string, public path: string) {
constructor(public originalMessage: string, public code: string = '', public path: string = '') {
super(`Serialization failed. ${!path ? '' : (path && path.startsWith('.') ? path.slice(1) : path) + ': '}` + originalMessage);
}
}
Expand Down Expand Up @@ -522,13 +524,31 @@ export class TemplateState {
* @example
* ```typescript
* serializer.deserializeRegistry.registerClass(Date, (type, state) => {
* state.convert((v) => new Date(v));
* // make sure to check `v` as it is any!
* state.convert((v: any) => {
* if ('number' !== typeof v) throw new SerializationError('Expected number');
* return new Date(v);
* });
* });
*
* serializer.serializeRegistry.registerClass(Date, (type, state) => {
* // in serialization `v` is always the specific type
* state.convert((v: Date) => v.getTime());
* });
* ```
*/
convert(callback: (value: any) => any) {
const converter = this.setVariable('convert', callback);
this.addSetter(`${converter}(${this.accessor})`);
this.addCodeForSetter(`
try {
${this.setter} = ${converter}(${this.accessor});
} catch (error) {
if (error instanceof SerializationError) {
error.path = ${collapsePath(this.path)} + (error.path ? '.' + error.path : '');
}
throw error;
}
`);
}

/**
Expand Down
42 changes: 41 additions & 1 deletion packages/type/tests/serializer-api.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@jest/globals';
import { EmptySerializer, executeTemplates, serializer, Serializer, TemplateRegistry, TemplateState, TypeGuardRegistry } from '../src/serializer.js';
import { EmptySerializer, executeTemplates, SerializationError, serializer, Serializer, TemplateRegistry, TemplateState, TypeGuardRegistry } from '../src/serializer.js';
import { ReflectionKind, stringifyResolvedType } from '../src/reflection/type.js';
import { CompilerContext } from '@deepkit/core';
import { cast, deserialize, serialize } from '../src/serializer-facade.js';
Expand Down Expand Up @@ -92,6 +92,46 @@ test('new serializer easy mode', () => {
expect(user.created).toBeInstanceOf(Date);
});

test('pointer example', () => {
class Point {
constructor(public x: number, public y: number) {
}
}

// deserialize means from JSON to (class) instance.
serializer.deserializeRegistry.registerClass(Point, (type, state) => {
state.convert((v: any) => {
// at this point `v` could be anything (except undefined), so we need to check
if (!Array.isArray(v)) throw new SerializationError('Expected array');
if (v.length !== 2) throw new SerializationError('Expected array with two elements');
if (typeof v[0] !== 'number' || typeof v[1] !== 'number') throw new SerializationError('Expected array with two numbers');
return new Point(v[0], v[1]);
});
});

serializer.serializeRegistry.registerClass(Point, (type, state) => {
state.convert((v: Point) => {
// at this point `v` is always a Point instance
return [v.x, v.y];
});
});

// cast and deserialize use `serializer` by default
const point = cast<Point>([1, 2], undefined, serializer);
expect(point).toBeInstanceOf(Point);
expect(point.x).toBe(1);
expect(point.y).toBe(2);

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

// serialize uses `serializer` by default
const json = serialize<Point>(point);
expect(json).toEqual([1, 2]);
});

test('parent types', () => {
type A = 'a' | 'b';
type B = A | 'c';
Expand Down

0 comments on commit 23781a1

Please sign in to comment.