Skip to content

Commit 0fe042a

Browse files
authored
feat(javascript): simplify the definition of object type description (#2058)
## What does this PR do? It is hard to describe the type of a object, we should write some code like this: ```JavaScript Type.object("example.foo", { a: Type.string() }) ``` The `Type.xxx` APIs are very flexible but not easy to use. This PR add improve these APIS to help simplifing the type definition. Now we can definition object type like this: ```JavaScript @Type.object("example.foo") class Foo { @Type.int32() a: number; } const fury = new Fury({ refTracking: true }); const { serialize, deserialize } = fury.registerSerializer(Foo); const foo = new Foo(); foo.a = 123; const input = serialize(foo); const result = deserialize( input ); expect(result instanceof Foo); expect(result).toEqual({ a: 123 }) ``` The result of `Type.xxx()` serves both as a decorator function and as a description object. It is totally compatible with `Type.object("example.foo", { a: Type.string() })` Note that decoration APIs can not detect the type of a anonymous object. providing anonymous object like `{a: 123}` still will cause error.
1 parent a6e60a5 commit 0fe042a

13 files changed

+254
-94
lines changed

javascript/jest.config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ module.exports = {
3232
transform: {
3333
'\\.ts$': ['ts-jest', {
3434
tsconfig: {
35-
target: "ES2021"
35+
target: "ES2021",
36+
experimentalDecorators: true
3637
},
3738
diagnostics: {
3839
ignoreCodes: [151001]

javascript/packages/fury/lib/classResolver.ts

+23-10
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* under the License.
1818
*/
1919

20-
import { InternalSerializerType, Serializer } from "./type";
20+
import { FuryClsInfoSymbol, InternalSerializerType, ObjectFuryClsInfo, Serializer } from "./type";
2121
import { fromString } from "./platformBuffer";
2222
import { x64hash128 } from "./murmurHash3";
2323
import { BinaryWriter } from "./writer";
@@ -78,9 +78,7 @@ const uninitSerialize = {
7878

7979
export default class SerializerResolver {
8080
private internalSerializer: Serializer[] = new Array(300);
81-
private customSerializer: { [key: string]: Serializer } = {
82-
};
83-
81+
private customSerializer: Map<string | (new () => any), Serializer> = new Map();
8482
private readStringPool: LazyString[] = [];
8583
private writeStringCount = 0;
8684
private writeStringIndex: number[] = [];
@@ -161,17 +159,25 @@ export default class SerializerResolver {
161159
return this.internalSerializer[id];
162160
}
163161

164-
registerSerializerByTag(tag: string, serializer: Serializer = uninitSerialize) {
165-
if (this.customSerializer[tag]) {
166-
Object.assign(this.customSerializer[tag], serializer);
162+
private registerCustomSerializer(tagOrConstructor: string | (new () => any), serializer: Serializer = uninitSerialize) {
163+
if (this.customSerializer.has(tagOrConstructor)) {
164+
Object.assign(this.customSerializer.get(tagOrConstructor)!, serializer);
167165
} else {
168-
this.customSerializer[tag] = { ...serializer };
166+
this.customSerializer.set(tagOrConstructor, { ...serializer });
169167
}
170-
return this.customSerializer[tag];
168+
return this.customSerializer.get(tagOrConstructor);
169+
}
170+
171+
registerSerializerByTag(tag: string, serializer: Serializer = uninitSerialize) {
172+
this.registerCustomSerializer(tag, serializer);
173+
}
174+
175+
registerSerializerByConstructor(constructor: new () => any, serializer: Serializer = uninitSerialize) {
176+
this.registerCustomSerializer(constructor, serializer);
171177
}
172178

173179
getSerializerByTag(tag: string) {
174-
return this.customSerializer[tag];
180+
return this.customSerializer.get(tag)!;
175181
}
176182

177183
static tagBuffer(tag: string) {
@@ -232,6 +238,7 @@ export default class SerializerResolver {
232238
}
233239

234240
getSerializerByData(v: any) {
241+
// internal types
235242
if (typeof v === "number") {
236243
return this.numberSerializer;
237244
}
@@ -263,6 +270,12 @@ export default class SerializerResolver {
263270
if (v instanceof Set) {
264271
return this.setSerializer;
265272
}
273+
274+
// custome types
275+
if (typeof v === "object" && v !== null && FuryClsInfoSymbol in v) {
276+
return this.customSerializer.get((v[FuryClsInfoSymbol] as ObjectFuryClsInfo).constructor)!;
277+
}
278+
266279
throw new Error(`Failed to detect the Fury type from JavaScript type: ${typeof v}`);
267280
}
268281

javascript/packages/fury/lib/description.ts

+116-62
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* under the License.
1818
*/
1919

20-
import { InternalSerializerType } from "./type";
20+
import { FuryClsInfoSymbol, InternalSerializerType, ObjectFuryClsInfo } from "./type";
2121

2222
export interface TypeDescription {
2323
type: InternalSerializerType;
@@ -28,6 +28,7 @@ export interface ObjectTypeDescription extends TypeDescription {
2828
options: {
2929
props: { [key: string]: TypeDescription };
3030
tag: string;
31+
withConstructor?: false;
3132
};
3233
}
3334

@@ -274,181 +275,234 @@ export type ResultType<T> = T extends {
274275
type: InternalSerializerType.ONEOF;
275276
} ? OneofResult<T> : unknown;
276277

278+
type DecorationWithDescription<T> = ((target: any, key?: string | { name?: string }) => void) & T;
279+
280+
const makeDescriptionWithDecoration = <T extends TypeDescription>(description: T): DecorationWithDescription<T> => {
281+
function decoration(target: any, key?: string | { name?: string }) {
282+
if (key === undefined) {
283+
initMeta(target, description as unknown as ObjectTypeDescription);
284+
} else {
285+
const keyString = typeof key === "string" ? key : key?.name;
286+
if (!keyString) {
287+
throw new Error("Decorators can only be placed on classes and fields");
288+
}
289+
addField(target.constructor, keyString, description);
290+
}
291+
}
292+
decoration.toJSON = function () {
293+
return JSON.stringify(description);
294+
};
295+
Object.entries(description).map(([key, value]: any) => {
296+
Object.defineProperty(decoration, key, {
297+
enumerable: true,
298+
get() {
299+
return value;
300+
},
301+
});
302+
});
303+
return decoration as unknown as DecorationWithDescription<T>;
304+
};
305+
277306
export const Type = {
278307
any() {
279-
return {
308+
return makeDescriptionWithDecoration({
280309
type: InternalSerializerType.ANY as const,
281-
};
310+
});
282311
},
283312
enum<T1 extends { [key: string]: any }>(t1: T1) {
284-
return {
313+
return makeDescriptionWithDecoration({
285314
type: InternalSerializerType.ENUM as const,
286315
options: {
287316
inner: t1,
288317
},
289-
};
318+
});
290319
},
291320
oneof<T extends { [key: string]: TypeDescription }>(inner?: T) {
292-
return {
321+
return makeDescriptionWithDecoration({
293322
type: InternalSerializerType.ONEOF as const,
294323
options: {
295324
inner,
296325
},
297-
};
326+
});
298327
},
299328
string() {
300-
return {
329+
return makeDescriptionWithDecoration({
301330
type: InternalSerializerType.STRING as const,
302-
};
331+
});
303332
},
304333
array<T extends TypeDescription>(def: T) {
305-
return {
334+
return makeDescriptionWithDecoration({
306335
type: InternalSerializerType.ARRAY as const,
307336
options: {
308337
inner: def,
309338
},
310-
};
339+
});
311340
},
312341
tuple<T1 extends readonly [...readonly TypeDescription[]]>(t1: T1) {
313-
return {
342+
return makeDescriptionWithDecoration({
314343
type: InternalSerializerType.TUPLE as const,
315344
options: {
316345
inner: t1,
317346
},
318-
};
347+
});
319348
},
320349
map<T1 extends TypeDescription, T2 extends TypeDescription>(
321350
key: T1,
322351
value: T2
323352
) {
324-
return {
353+
return makeDescriptionWithDecoration({
325354
type: InternalSerializerType.MAP as const,
326355
options: {
327356
key,
328357
value,
329358
},
330-
};
359+
});
331360
},
332361
set<T extends TypeDescription>(key: T) {
333-
return {
362+
return makeDescriptionWithDecoration({
334363
type: InternalSerializerType.SET as const,
335364
options: {
336365
key,
337366
},
338-
};
367+
});
339368
},
340369
bool() {
341-
return {
370+
return makeDescriptionWithDecoration({
342371
type: InternalSerializerType.BOOL as const,
343-
};
372+
});
344373
},
345-
object<T extends { [key: string]: TypeDescription }>(tag: string, props?: T) {
346-
return {
374+
object<T extends { [key: string]: TypeDescription }>(tag: string, props?: T, withConstructor = false) {
375+
return makeDescriptionWithDecoration({
347376
type: InternalSerializerType.OBJECT as const,
348377
options: {
349378
tag,
350379
props,
380+
withConstructor,
351381
},
352-
};
382+
});
353383
},
354384
int8() {
355-
return {
385+
return makeDescriptionWithDecoration({
356386
type: InternalSerializerType.INT8 as const,
357-
};
387+
});
358388
},
359389
int16() {
360-
return {
390+
return makeDescriptionWithDecoration({
361391
type: InternalSerializerType.INT16 as const,
362-
};
392+
});
363393
},
364394
int32() {
365-
return {
395+
return makeDescriptionWithDecoration({
366396
type: InternalSerializerType.INT32 as const,
367-
};
397+
});
368398
},
369399
varInt32() {
370-
return {
400+
return makeDescriptionWithDecoration({
371401
type: InternalSerializerType.VAR_INT32 as const,
372-
};
402+
});
373403
},
374404
int64() {
375-
return {
405+
return makeDescriptionWithDecoration({
376406
type: InternalSerializerType.INT64 as const,
377-
};
407+
});
378408
},
379409
sliInt64() {
380-
return {
410+
return makeDescriptionWithDecoration({
381411
type: InternalSerializerType.SLI_INT64 as const,
382-
};
412+
});
383413
},
384414
float16() {
385-
return {
415+
return makeDescriptionWithDecoration({
386416
type: InternalSerializerType.FLOAT16 as const,
387-
};
417+
});
388418
},
389419
float32() {
390-
return {
420+
return makeDescriptionWithDecoration({
391421
type: InternalSerializerType.FLOAT32 as const,
392-
};
422+
});
393423
},
394424
float64() {
395-
return {
425+
return makeDescriptionWithDecoration({
396426
type: InternalSerializerType.FLOAT64 as const,
397-
};
427+
});
398428
},
399429
binary() {
400-
return {
430+
return makeDescriptionWithDecoration({
401431
type: InternalSerializerType.BINARY as const,
402-
};
432+
});
403433
},
404434
duration() {
405-
return {
435+
return makeDescriptionWithDecoration({
406436
type: InternalSerializerType.DURATION as const,
407-
};
437+
});
408438
},
409439
timestamp() {
410-
return {
440+
return makeDescriptionWithDecoration({
411441
type: InternalSerializerType.TIMESTAMP as const,
412-
};
442+
});
413443
},
414444
boolArray() {
415-
return {
445+
return makeDescriptionWithDecoration({
416446
type: InternalSerializerType.BOOL_ARRAY as const,
417-
};
447+
});
418448
},
419449
int8Array() {
420-
return {
450+
return makeDescriptionWithDecoration({
421451
type: InternalSerializerType.INT8_ARRAY as const,
422-
};
452+
});
423453
},
424454
int16Array() {
425-
return {
455+
return makeDescriptionWithDecoration({
426456
type: InternalSerializerType.INT16_ARRAY as const,
427-
};
457+
});
428458
},
429459
int32Array() {
430-
return {
460+
return makeDescriptionWithDecoration({
431461
type: InternalSerializerType.INT32_ARRAY as const,
432-
};
462+
});
433463
},
434464
int64Array() {
435-
return {
465+
return makeDescriptionWithDecoration({
436466
type: InternalSerializerType.INT64_ARRAY as const,
437-
};
467+
});
438468
},
439469
float16Array() {
440-
return {
470+
return makeDescriptionWithDecoration({
441471
type: InternalSerializerType.FLOAT16_ARRAY as const,
442-
};
472+
});
443473
},
444474
float32Array() {
445-
return {
475+
return makeDescriptionWithDecoration({
446476
type: InternalSerializerType.FLOAT32_ARRAY as const,
447-
};
477+
});
448478
},
449479
float64Array() {
450-
return {
480+
return makeDescriptionWithDecoration({
451481
type: InternalSerializerType.FLOAT64_ARRAY as const,
452-
};
482+
});
453483
},
454484
};
485+
486+
const initMeta = (target: new () => any, description: ObjectTypeDescription) => {
487+
if (!target.prototype) {
488+
target.prototype = {};
489+
}
490+
target.prototype[FuryClsInfoSymbol] = {
491+
toObjectDescription() {
492+
if (targetFields.has(target)) {
493+
return Type.object(description.options.tag, targetFields.get(target), true);
494+
}
495+
return Type.object(description.options.tag, {}, true);
496+
},
497+
constructor: target,
498+
} as ObjectFuryClsInfo;
499+
};
500+
501+
const targetFields = new WeakMap<new () => any, { [key: string]: TypeDescription }>();
502+
503+
const addField = (target: new () => any, key: string, des: TypeDescription) => {
504+
if (!targetFields.has(target)) {
505+
targetFields.set(target, {});
506+
}
507+
targetFields.get(target)![key] = des;
508+
};

0 commit comments

Comments
 (0)