Skip to content

Commit

Permalink
Merge pull request #616 from tuanchauict/serialization-js
Browse files Browse the repository at this point in the history
Add Custom Serialization Decorators and Serializable Class
  • Loading branch information
tuanchauict authored Dec 10, 2024
2 parents f39b122 + c6b0507 commit 594561f
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright (c) 2024, tuanchauict
*/

import { Jsonizable, serializer, Serializer, SerialName } from "$mono/shape/serialization/serializable";
import { describe, expect, it } from "vitest";

enum AnEnum {
A,
B,
C
}

const AnEnumSerializer = {
serialize: (value: AnEnum): string => {
switch (value) {
case AnEnum.A:
return "xA";
case AnEnum.B:
return "xB";
case AnEnum.C:
return "xC";
}
},

deserialize: (value: string): AnEnum => {
switch (value) {
case "xA":
return AnEnum.A;
case "xB":
return AnEnum.B;
case "xC":
return AnEnum.C;
default:
throw new Error(`Unrecognizable value ${value}`);
}
},
}

@Jsonizable
class Bar {
@SerialName("a")
public aString: string = "";

public aNumber: number = 123;

private constructor() {
}

static create(aString: string, aNumber: number): Bar {
const instance = new Bar();
instance.aString = aString;
instance.aNumber = aNumber;
return instance;
}
}

class Pair {
public first: number = 0;
public second: number = 0;

constructor(first: number, second: number) {
this.first = first;
this.second = second;
}
}

const PairSerializer = serializer(
(value: Pair) => `${value.first}|${value.second}`,
(value: string) => {
const [first, second] = value.split("|").map(Number);
if (isNaN(first) || isNaN(second)) {
throw new Error(`Invalid Pair format: ${value}`);
}
return new Pair(first, second);
}
)

@Jsonizable
class Foo {
@SerialName("b")
public bar: Bar = Bar.create("default", 0);

public aBoolean: boolean = true;

@Serializer(AnEnumSerializer)
public anEnum: AnEnum = AnEnum.A;

@SerialName("p")
@Serializer(PairSerializer)
public pair: Pair = new Pair(1, 2);

private constructor() {
}

static create(bar: Bar, aBoolean: boolean, anEnum: AnEnum): Foo {
const instance = new Foo();
instance.bar = bar;
instance.aBoolean = aBoolean;
instance.anEnum = anEnum;
return instance;
}
}

describe("Serializable", () => {
it("should serialize and deserialize correctly", () => {
const foo = Foo.create(Bar.create("foo-bar", 100), false, AnEnum.C);
// @ts-ignore
expect(foo.toJson()).toStrictEqual({
b: { a: "foo-bar", aNumber: 100 },
aBoolean: false,
anEnum: 'xC',
p: "1|2",
});

const json = {
b: { a: "a new string", aNumber: 999 },
aBoolean: false,
anEnum: 'xB',
p: "3|4",
}
// @ts-ignore
const foo2 = Foo.fromJson(json);
expect(foo2.bar.aString).toBe("a new string");
expect(foo2.bar.aNumber).toBe(999);
expect(foo2.aBoolean).toBe(false);
expect(foo2.anEnum).toBe(AnEnum.B);
expect(foo2.pair.first).toBe(3);
expect(foo2.pair.second).toBe(4);
})
})
123 changes: 123 additions & 0 deletions monosketch-svelte/src/lib/mono/shape/serialization/serializable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (c) 2024, tuanchauict
*/

/**
* An interface for custom serialization and deserialization.
*/
export interface Serializable {
serialize(value: any): any;

deserialize(value: any): any;
}

export const serializer =
(serialize: (value: any) => any, deserialize: (value: any) => any): Serializable => ({ serialize, deserialize });

/**
* A decorator to define a serial name for a field of a class.
*
* @param name - The serial name of the field. For example, if the field name is `foo` and the serial name is `f`,
* the field will be serialized as `{f: value}` instead of `{foo: value}`.
* @see Serializable
*/
export function SerialName(name: string) {
return function (target: any, propertyKey: string | symbol) {
if (!target.constructor.serialNames) {
target.constructor.serialNames = {};
}
target.constructor.serialNames[propertyKey] = name;
};
}

/**
* A decorator to define a serializer for a field of a class.
*
* @param serializer - The serializer object.
* @see Serializable
*/
export function Serializer(serializer: Serializable) {
return function (target: any, propertyKey: string | symbol) {
if (!target.constructor.serializers) {
target.constructor.serializers = {};
}
target.constructor.serializers[propertyKey] = serializer;
};
}

/**
* A decorator to make a class serializable with customizable field names and serializer functions.
* This decorator adds `toJson` and `fromJson` methods to the class, allowing instances to be
* serialized to JSON and deserialized from JSON.
*
* @param constructor - The constructor function of the class to be decorated.
*/
export function Jsonizable(constructor: Function) {
// @ts-ignore
if (!constructor.serializers) {
// @ts-ignore
constructor.serializers = {};
}
// @ts-ignore
if (!constructor.serialNames) {
// @ts-ignore
constructor.serialNames = {};
}

// @ts-ignore
const serialNames = constructor.serialNames;
// @ts-ignore
const serializers = constructor.serializers;

constructor.prototype.toJson = function () {
const json: any = {};
const instance = this;
for (const key in instance) {
if (!instance.hasOwnProperty(key)) {
continue;
}
// If the key is not defined in serialNames, use the key itself
const serializedKey = serialNames[key] ?? key;

// 1st: Check if the field has a serializer
// 2nd: Check if the field has a toJson method
// 3rd: Use the value directly
if (serializers[key]) {
json[serializedKey] = serializers[key].serialize(instance[key]);
} else if (instance[key].toJson) {
json[serializedKey] = instance[key].toJson();
} else {
json[serializedKey] = instance[key];
}
}
return json;
};

// @ts-ignore
constructor.fromJson = function (data: any) {
// @ts-ignore
const instance = new constructor();
for (const key of Object.keys(instance)) {
// If the key is not defined in serialNames, use the key itself
const serializedKey = serialNames[key] ?? key;
const value = data[serializedKey];
if (value === undefined) {
continue;
}

const field = instance[key];
// 1st: Check if the field has a serializer
// 2nd: Check if the field has a fromJson method
// 3rd: Use the value directly
if (serializers[key]) {
instance[key] = serializers[key].deserialize(value);
} else if (field.constructor && field.constructor.fromJson) {
instance[key] = field.constructor.fromJson(value);
} else {
instance[key] = value;
}
}

return instance;
};
}
1 change: 1 addition & 0 deletions monosketch-svelte/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"experimentalDecorators": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
Expand Down

0 comments on commit 594561f

Please sign in to comment.