From b4c7a9ba2b0e081a38e877a02813cb4d46bc118a Mon Sep 17 00:00:00 2001 From: rowan Date: Fri, 23 May 2025 21:39:46 -0500 Subject: [PATCH] add rename options for ser/de --- dist/de/forward.d.ts | 8 +- dist/de/forward.js | 12 +- dist/de/generic.d.ts | 21 ++- dist/de/generic.js | 196 ++++++++++++++++++++++++-- dist/de/impl.js | 4 +- dist/de/interface.d.ts | 47 ++++--- dist/de/interface.js | 24 ++++ dist/decorator.d.ts | 2 + dist/decorator.js | 34 +++++ dist/index.d.ts | 2 + dist/index.js | 2 + dist/option.d.ts | 71 ++++++++++ dist/option.js | 209 ++++++++++++++++++++++++++++ dist/registry.d.ts | 6 +- dist/ser/impl.d.ts | 2 +- dist/ser/impl.js | 5 +- dist/utils.d.ts | 1 - dist/utils.js | 4 - src/de/forward.ts | 16 +-- src/de/generic.ts | 190 +++++++++++++++++++++++-- src/de/impl.ts | 3 +- src/de/interface.ts | 79 ++++++++--- src/decorator.ts | 44 ++++++ src/index.ts | 2 + src/{ser/decorator.ts => option.ts} | 106 +++++++++++--- src/registry.ts | 4 +- src/ser/impl.ts | 7 +- src/utils.ts | 4 - 28 files changed, 981 insertions(+), 124 deletions(-) create mode 100644 dist/decorator.d.ts create mode 100644 dist/decorator.js create mode 100644 dist/option.d.ts create mode 100644 dist/option.js create mode 100644 src/decorator.ts rename src/{ser/decorator.ts => option.ts} (60%) diff --git a/dist/de/forward.d.ts b/dist/de/forward.d.ts index 69396a3..dbfd7ec 100644 --- a/dist/de/forward.d.ts +++ b/dist/de/forward.d.ts @@ -1,13 +1,13 @@ import { Deserialize, IDeserializer, IterableAccess, IVisitor, MapAccess } from './interface'; export declare class ForwardMapAccess extends MapAccess { - private readonly keys; - private readonly values; + private readonly _keys; + private readonly _values; private kindex; private vindex; constructor(keys: string[], values: any[]); static fromObject(obj: object): ForwardMapAccess; - nextKeySeed(_seed: K): IteratorResult; - nextValueSeed(_seed: V): IteratorResult; + nextKeySeed>(_seed: K): IteratorResult; + nextValueSeed>(_seed: V): IteratorResult; nextKey(): IteratorResult; nextValue(): IteratorResult; } diff --git a/dist/de/forward.js b/dist/de/forward.js index 9afdc72..a2d7b94 100644 --- a/dist/de/forward.js +++ b/dist/de/forward.js @@ -6,13 +6,13 @@ const interface_1 = require("./interface"); class ForwardMapAccess extends interface_1.MapAccess { constructor(keys, values) { super(); - Object.defineProperty(this, "keys", { + Object.defineProperty(this, "_keys", { enumerable: true, configurable: true, writable: true, value: void 0 }); - Object.defineProperty(this, "values", { + Object.defineProperty(this, "_values", { enumerable: true, configurable: true, writable: true, @@ -30,8 +30,8 @@ class ForwardMapAccess extends interface_1.MapAccess { writable: true, value: 0 }); - this.keys = keys; - this.values = values; + this._keys = keys; + this._values = values; } static fromObject(obj) { return new ForwardMapAccess(Object.keys(obj), Object.values(obj)); @@ -44,7 +44,7 @@ class ForwardMapAccess extends interface_1.MapAccess { } nextKey() { if (this.kindex < this.keys.length) { - return utils_1.IterResult.Next(this.keys[this.kindex++]); + return utils_1.IterResult.Next(this._keys[this.kindex++]); } else { return utils_1.IterResult.Done(); @@ -52,7 +52,7 @@ class ForwardMapAccess extends interface_1.MapAccess { } nextValue() { if (this.vindex < this.values.length) { - return utils_1.IterResult.Next(this.values[this.vindex++]); + return utils_1.IterResult.Next(this._values[this.vindex++]); } else { return utils_1.IterResult.Done(); diff --git a/dist/de/generic.d.ts b/dist/de/generic.d.ts index 6b900a1..5e507e6 100644 --- a/dist/de/generic.d.ts +++ b/dist/de/generic.d.ts @@ -1,3 +1,4 @@ +import { SerdeOptions } from '../option'; import { IDeserializer, IIterableAccess, IMapAccess, IVisitor } from './interface'; export declare class GenericSeed { readonly visitor: IVisitor; @@ -5,7 +6,10 @@ export declare class GenericSeed { static deserialize(deserializer: D, visitor?: IVisitor): T; deserialize(deserializer: D): T; } -export declare class GenericVisitor implements IVisitor { +export declare class ProxyVisitor implements IVisitor { + private overrides?; + private options?; + constructor(overrides?: Partial>, options?: SerdeOptions); visitBoolean(value: boolean): T; visitNumber(value: number): T; visitBigInt(value: bigint): T; @@ -15,3 +19,18 @@ export declare class GenericVisitor implements IVisitor { visitObject(access: IMapAccess): T; visitIterable(access: IIterableAccess): T; } +export declare class ProxyDeserializer implements IDeserializer { + private readonly deserializer; + private readonly options?; + constructor(deserializer: IDeserializer, options?: SerdeOptions); + deserializeAny>(visitor: V): T; + deserializeBoolean>(visitor: V): T; + deserializeNumber>(visitor: V): T; + deserializeBigInt>(visitor: V): T; + deserializeString>(visitor: V): T; + deserializeSymbol>(visitor: V): T; + deserializeNull>(visitor: V): T; + deserializeObject>(visitor: V): T; + deserializeIterable>(visitor: V): T; + deserializeFunction>(visitor: V): T; +} diff --git a/dist/de/generic.js b/dist/de/generic.js index 4c0884a..c785e2d 100644 --- a/dist/de/generic.js +++ b/dist/de/generic.js @@ -1,8 +1,9 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.GenericVisitor = exports.GenericSeed = void 0; +exports.ProxyDeserializer = exports.ProxyVisitor = exports.GenericSeed = void 0; +const utils_1 = require("../utils"); class GenericSeed { - constructor(visitor = new GenericVisitor()) { + constructor(visitor = new ProxyVisitor()) { Object.defineProperty(this, "visitor", { enumerable: true, configurable: true, @@ -11,7 +12,7 @@ class GenericSeed { }); this.visitor = visitor; } - static deserialize(deserializer, visitor = new GenericVisitor()) { + static deserialize(deserializer, visitor = new ProxyVisitor()) { return deserializer.deserializeAny(visitor); } deserialize(deserializer) { @@ -19,34 +20,156 @@ class GenericSeed { } } exports.GenericSeed = GenericSeed; -class GenericVisitor { +class ProxyMapAccess { + constructor(access, options) { + Object.defineProperty(this, "access", { + enumerable: true, + configurable: true, + writable: true, + value: void 0 + }); + Object.defineProperty(this, "options", { + enumerable: true, + configurable: true, + writable: true, + value: void 0 + }); + if (access instanceof ProxyMapAccess) { + return this; + } + this.access = access; + this.options = options; + } + wrapResponse(result) { + var _a, _b, _c, _d; + if (result.done) { + return result; + } + else if ((0, utils_1.isString)(result.value)) { + const key = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.getDeserializationPropertyName(result.value)) !== null && _b !== void 0 ? _b : result.value; + return utils_1.IterResult.Next(key); + } + else if (Array.isArray(result.value)) { + const [alias, value] = result.value; + const key = (_d = (_c = this.options) === null || _c === void 0 ? void 0 : _c.getDeserializationPropertyName(alias)) !== null && _d !== void 0 ? _d : alias; + return utils_1.IterResult.Next([ + key, + value + ]); + } + else { + return result; + } + } + nextKeySeed(seed) { + return this.wrapResponse(this.access.nextKeySeed(seed)); + } + nextValueSeed(seed) { + return this.access.nextValueSeed(seed); + } + nextEntrySeed(kseed, vseed) { + return this.wrapResponse(this.access.nextEntrySeed(kseed, vseed)); + } + nextKey() { + return this.wrapResponse(this.access.nextKey()); + } + nextValue() { + return this.access.nextValue(); + } + nextEntry() { + return this.wrapResponse(this.access.nextEntry()); + } + sizeHint() { + var _a, _b; + return (_b = (_a = this.access).sizeHint) === null || _b === void 0 ? void 0 : _b.call(_a); + } + *generate(next) { + let item; + while ((item = next()) && !item.done) { + yield item.value; + } + } + keys(seed) { + return this.generate(seed == null ? + this.nextKey.bind(this) : + this.nextKeySeed.bind(this, seed)); + } + values(seed) { + return this.generate(seed == null ? + this.nextValue.bind(this) : + this.nextValueSeed.bind(this, seed)); + } + entries(kseed, vseed) { + return this.generate(kseed == null || vseed == null ? + this.nextEntry.bind(this) : + this.nextEntrySeed.bind(this, kseed, vseed)); + } + [Symbol.iterator]() { + return this.entries(); + } +} +class ProxyVisitor { + constructor(overrides, options) { + Object.defineProperty(this, "overrides", { + enumerable: true, + configurable: true, + writable: true, + value: void 0 + }); + Object.defineProperty(this, "options", { + enumerable: true, + configurable: true, + writable: true, + value: void 0 + }); + if (overrides instanceof ProxyVisitor) { + return overrides; + } + this.overrides = overrides; + this.options = options; + } visitBoolean(value) { - return value; + var _a, _b, _c; + return (_c = (_b = (_a = this.overrides) === null || _a === void 0 ? void 0 : _a.visitBoolean) === null || _b === void 0 ? void 0 : _b.call(_a, value)) !== null && _c !== void 0 ? _c : value; } visitNumber(value) { - return value; + var _a, _b, _c; + return (_c = (_b = (_a = this.overrides) === null || _a === void 0 ? void 0 : _a.visitNumber) === null || _b === void 0 ? void 0 : _b.call(_a, value)) !== null && _c !== void 0 ? _c : value; } visitBigInt(value) { - return value; + var _a, _b, _c; + return (_c = (_b = (_a = this.overrides) === null || _a === void 0 ? void 0 : _a.visitBigInt) === null || _b === void 0 ? void 0 : _b.call(_a, value)) !== null && _c !== void 0 ? _c : value; } visitString(value) { - return value; + var _a, _b, _c; + return (_c = (_b = (_a = this.overrides) === null || _a === void 0 ? void 0 : _a.visitString) === null || _b === void 0 ? void 0 : _b.call(_a, value)) !== null && _c !== void 0 ? _c : value; } visitSymbol(value) { - return value; + var _a, _b, _c; + return (_c = (_b = (_a = this.overrides) === null || _a === void 0 ? void 0 : _a.visitSymbol) === null || _b === void 0 ? void 0 : _b.call(_a, value)) !== null && _c !== void 0 ? _c : value; } visitNull() { - return null; + var _a, _b, _c; + return (_c = (_b = (_a = this.overrides) === null || _a === void 0 ? void 0 : _a.visitNull) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : null; } visitObject(access) { + var _a, _b; + const proxy = new ProxyMapAccess(access, this.options); + if ((0, utils_1.isFunction)((_a = this.overrides) === null || _a === void 0 ? void 0 : _a.visitObject)) { + return (_b = this.overrides) === null || _b === void 0 ? void 0 : _b.visitObject(proxy); + } const result = []; let entry; - while ((entry = access.nextEntry()) && !entry.done) { + while ((entry = proxy.nextEntry()) && !entry.done) { result.push(entry.value); } return Object.fromEntries(result); } visitIterable(access) { + var _a, _b; + if ((0, utils_1.isFunction)((_a = this.overrides) === null || _a === void 0 ? void 0 : _a.visitIterable)) { + return (_b = this.overrides) === null || _b === void 0 ? void 0 : _b.visitIterable(access); + } const result = []; let element; while ((element = access.nextElement())) { @@ -55,4 +178,53 @@ class GenericVisitor { return result; } } -exports.GenericVisitor = GenericVisitor; +exports.ProxyVisitor = ProxyVisitor; +class ProxyDeserializer { + constructor(deserializer, options) { + Object.defineProperty(this, "deserializer", { + enumerable: true, + configurable: true, + writable: true, + value: void 0 + }); + Object.defineProperty(this, "options", { + enumerable: true, + configurable: true, + writable: true, + value: void 0 + }); + this.deserializer = deserializer; + this.options = options; + } + deserializeAny(visitor) { + return this.deserializer.deserializeAny(new ProxyVisitor(visitor, this.options)); + } + deserializeBoolean(visitor) { + return this.deserializer.deserializeBoolean(new ProxyVisitor(visitor, this.options)); + } + deserializeNumber(visitor) { + return this.deserializer.deserializeNumber(new ProxyVisitor(visitor, this.options)); + } + deserializeBigInt(visitor) { + return this.deserializer.deserializeBigInt(new ProxyVisitor(visitor, this.options)); + } + deserializeString(visitor) { + return this.deserializer.deserializeString(new ProxyVisitor(visitor, this.options)); + } + deserializeSymbol(visitor) { + return this.deserializer.deserializeSymbol(new ProxyVisitor(visitor, this.options)); + } + deserializeNull(visitor) { + return this.deserializer.deserializeNull(new ProxyVisitor(visitor, this.options)); + } + deserializeObject(visitor) { + return this.deserializer.deserializeObject(new ProxyVisitor(visitor, this.options)); + } + deserializeIterable(visitor) { + return this.deserializer.deserializeIterable(new ProxyVisitor(visitor, this.options)); + } + deserializeFunction(visitor) { + return this.deserializer.deserializeFunction(new ProxyVisitor(visitor, this.options)); + } +} +exports.ProxyDeserializer = ProxyDeserializer; diff --git a/dist/de/impl.js b/dist/de/impl.js index c4d4d4c..2ce8d5b 100644 --- a/dist/de/impl.js +++ b/dist/de/impl.js @@ -2,12 +2,14 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.deserialize = deserialize; const registry_1 = require("../registry"); +const generic_1 = require("./generic"); function deserialize(deserializer, into, registry = registry_1.GlobalRegistry) { + var _a; const de = registry.deserializers.get(into); if (de == null) { throw new ReferenceError(`No deserializer for ${into.name}`); } else { - return de(deserializer); + return de(new generic_1.ProxyDeserializer(deserializer, (_a = into === null || into === void 0 ? void 0 : into[Symbol.metadata]) === null || _a === void 0 ? void 0 : _a.serde)); } } diff --git a/dist/de/interface.d.ts b/dist/de/interface.d.ts index 46fc960..b18158b 100644 --- a/dist/de/interface.d.ts +++ b/dist/de/interface.d.ts @@ -1,20 +1,29 @@ -import { Nullable } from "../utils"; +import { Nullable } from '../utils'; export interface IMapAccess { - nextKeySeed(seed: K): IteratorResult; - nextValueSeed(seed: V): IteratorResult; - nextEntrySeed(kseed: K, vseed: V): IteratorResult<[TK, TV]>; + nextKeySeed>(seed: K): IteratorResult; + nextValueSeed>(seed: V): IteratorResult; + nextEntrySeed, V extends Deserialize>(kseed: K, vseed: V): IteratorResult<[TK, TV]>; nextKey(): IteratorResult; nextValue(): IteratorResult; nextEntry(): IteratorResult<[K, V]>; sizeHint?(): Nullable; + entries>(seed?: K): Iterator; + values>(seed?: V): Iterator; + entries, V extends Deserialize>(kseed: K, vseed: V): Iterator<[TK, TV]>; + [Symbol.iterator](): Iterator<[K, V]>; } export declare abstract class MapAccess { - abstract nextKeySeed(seed: K): IteratorResult; - abstract nextValueSeed(seed: V): IteratorResult; - nextEntrySeed(kseed: K, vseed: V): IteratorResult<[TK, TV]>; + abstract nextKeySeed>(seed: K): IteratorResult; + abstract nextValueSeed>(seed: V): IteratorResult; + nextEntrySeed, V extends Deserialize>(kseed: K, vseed: V): IteratorResult<[TK, TV]>; abstract nextKey(): IteratorResult; abstract nextValue(): IteratorResult; nextEntry(): IteratorResult<[K, V]>; + private generate; + keys>(seed?: K): Iterator; + values>(seed?: V): Iterator; + entries, V extends Deserialize>(kseed?: K, vseed?: V): Iterator<[TK, TV]>; + [Symbol.iterator](): Iterator; } export interface IIterableAccess { nextElement(): IteratorResult; @@ -34,17 +43,17 @@ export interface IVisitor { visitIterable(access: IIterableAccess): T; } export interface IDeserializer { - deserializeAny>(visitor: V): T; - deserializeBoolean>(visitor: V): T; - deserializeNumber>(visitor: V): T; - deserializeBigInt>(visitor: V): T; - deserializeString>(visitor: V): T; - deserializeSymbol>(visitor: V): T; - deserializeNull>(visitor: V): T; - deserializeObject>(visitor: V): T; - deserializeIterable>(visitor: V): T; - deserializeFunction>(visitor: V): T; + deserializeAny(visitor: Partial>): T; + deserializeBoolean(visitor: Partial>): T; + deserializeNumber(visitor: Partial>): T; + deserializeBigInt(visitor: Partial>): T; + deserializeString(visitor: Partial>): T; + deserializeSymbol(visitor: Partial>): T; + deserializeNull(visitor: Partial>): T; + deserializeObject(visitor: Partial>): T; + deserializeIterable(visitor: Partial>): T; + deserializeFunction(visitor: Partial>): T; } -export interface Deserialize { - (deserializer: IDeserializer): T; +export interface Deserialize { + (deserializer: IDeserializer): T; } diff --git a/dist/de/interface.js b/dist/de/interface.js index 7ff51dc..5ce1694 100644 --- a/dist/de/interface.js +++ b/dist/de/interface.js @@ -23,6 +23,30 @@ class MapAccess { } return utils_1.IterResult.Done(); } + *generate(next) { + let item; + while ((item = next())) { + yield item.value; + } + } + keys(seed) { + return this.generate(seed == null ? + this.nextKey.bind(this) : + this.nextKeySeed.bind(this, seed)); + } + values(seed) { + return this.generate(seed == null ? + this.nextValue.bind(this) : + this.nextValueSeed.bind(this, seed)); + } + entries(kseed, vseed) { + return this.generate(kseed == null || vseed == null ? + this.nextEntry.bind(this) : + this.nextEntrySeed.bind(this, kseed, vseed)); + } + [Symbol.iterator]() { + return this.entries(); + } } exports.MapAccess = MapAccess; class IterableAccess { diff --git a/dist/decorator.d.ts b/dist/decorator.d.ts new file mode 100644 index 0000000..68876ed --- /dev/null +++ b/dist/decorator.d.ts @@ -0,0 +1,2 @@ +import { ContainerOptions, PropertyOptions } from './option'; +export declare function serde(options: ContainerOptions | PropertyOptions): (target: any, context: DecoratorContext) => void; diff --git a/dist/decorator.js b/dist/decorator.js new file mode 100644 index 0000000..df7f75f --- /dev/null +++ b/dist/decorator.js @@ -0,0 +1,34 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.serde = serde; +const option_1 = require("./option"); +const ContextKind = Object.freeze({ + Class: 'class', + Method: 'method', + Getter: 'getter', + Setter: 'setter', + Field: 'field', + Accessor: 'accessor' +}); +function decorateClass(target, context, options) { + const serde = context.metadata.serde; + context.metadata.serde = new option_1.SerdeOptions(target, options, serde === null || serde === void 0 ? void 0 : serde.properties); +} +function decorateField(context, options) { + const serde = context.metadata.serde || new option_1.SerdeOptions(undefined); + serde.properties.set(context.name, options); + context.metadata.serde = serde; +} +function serde(options) { + return function (target, context) { + switch (context.kind) { + case ContextKind.Class: + decorateClass(target, context, options); + break; + case ContextKind.Field: + decorateField(context, options); + default: + break; + } + }; +} diff --git a/dist/index.d.ts b/dist/index.d.ts index 3dfe828..b787498 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,6 +1,8 @@ import '@tsmetadata/polyfill'; export * as ser from './ser/impl'; export * as de from './de/impl'; +export * from './decorator'; +export * from './option'; export * from './case'; export * from './registry'; export * from './utils'; diff --git a/dist/index.js b/dist/index.js index 5a99de4..8927ae9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -40,6 +40,8 @@ exports.de = exports.ser = void 0; require("@tsmetadata/polyfill"); exports.ser = __importStar(require("./ser/impl")); exports.de = __importStar(require("./de/impl")); +__exportStar(require("./decorator"), exports); +__exportStar(require("./option"), exports); __exportStar(require("./case"), exports); __exportStar(require("./registry"), exports); __exportStar(require("./utils"), exports); diff --git a/dist/option.d.ts b/dist/option.d.ts new file mode 100644 index 0000000..76715eb --- /dev/null +++ b/dist/option.d.ts @@ -0,0 +1,71 @@ +import { Deserialize } from './de'; +import { Serialize } from './ser'; +import { CaseConvention } from './case'; +import { Nullable } from './utils'; +export interface RenameOptions { + serialize?: string; + deserialize?: string; +} +export interface RenameAllOptions { + serialize?: CaseConvention; + deserialize?: CaseConvention; +} +export interface ContainerOptions { + rename?: RenameOptions | string; + renameAll?: RenameAllOptions | CaseConvention; + default?: () => any; + denyUnknownFields?: boolean; + tag?: string; + untagged?: boolean; +} +interface Predicate { + (value: T): boolean; +} +export interface SkipOptions { + serialize?: Predicate | boolean; + deserialize?: Predicate | boolean; +} +export interface PropertyOptions { + alias?: string | string[]; + default?: () => any; + flatten?: boolean; + rename?: RenameOptions | string; + skip?: SkipOptions | Predicate | boolean; + serializeWith?: Serialize; + deserializeWith?: Deserialize; +} +export declare const Stage: Readonly<{ + readonly Serialize: 0; + readonly Deserialize: 1; +}>; +export declare class PropertyMap extends Map { + private readonly options; + private readonly aliases; + constructor(options: SerdeOptions, iterable?: Iterable); + private setAliasesFromOptions; + set(key: string, value: PropertyOptions): this; + addAlias(alias: string | Iterable, key: string): void; + getFieldFromAlias(alias: string): string | undefined; + private buildAliasMap; +} +export type Stage = typeof Stage[keyof typeof Stage]; +export declare class SerdeOptions { + readonly target: Nullable; + readonly container: ContainerOptions; + readonly properties: PropertyMap; + constructor(target: any, container?: ContainerOptions, properties?: PropertyMap); + private getClassName; + getSerializedClassName(defaultName?: string): string | undefined; + getDeserializedClassName(defaultName?: string): string | undefined; + getCaseConvention(stage: Stage): CaseConvention | undefined; + applyCaseConvention(stage: Stage, property: string): string; + getPropertyRename(stage: Stage, property: string): string | undefined; + getSerializationPropertyName(property: string): string; + getNameFromAlias(alias: string): string | undefined; + getDeserializationPropertyName(property: string): string; + shouldSkip(stage: Stage, property: string, value: any): boolean; + shouldSerialize(property: string, value: any): boolean; + shouldDeserialize(property: string, value: any): boolean; + defaultFor(property: string): any; +} +export {}; diff --git a/dist/option.js b/dist/option.js new file mode 100644 index 0000000..c999b07 --- /dev/null +++ b/dist/option.js @@ -0,0 +1,209 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SerdeOptions = exports.PropertyMap = exports.Stage = void 0; +const case_1 = require("./case"); +const utils_1 = require("./utils"); +exports.Stage = Object.freeze({ + Serialize: 0, + Deserialize: 1 +}); +class PropertyMap extends Map { + constructor(options, iterable) { + super(iterable); + Object.defineProperty(this, "options", { + enumerable: true, + configurable: true, + writable: true, + value: void 0 + }); + Object.defineProperty(this, "aliases", { + enumerable: true, + configurable: true, + writable: true, + value: new Map() + }); + this.options = options; + this.buildAliasMap(); + } + setAliasesFromOptions(key, options) { + this.aliases.set(key, key); + if (options.alias != null) { + this.addAlias(options.alias, key); + } + if ((0, utils_1.isString)(options.rename)) { + this.aliases.set(options.rename, key); + } + else if ((0, utils_1.isNumber)(this.options.container.renameAll)) { + this.aliases.set(this.options.applyCaseConvention(exports.Stage.Deserialize, key), key); + } + } + set(key, value) { + super.set(key, value); + this.setAliasesFromOptions(key, value); + return this; + } + addAlias(alias, key) { + if ((0, utils_1.isString)(alias)) { + this.aliases.set(alias, key); + } + else if ((0, utils_1.isIterable)(alias)) { + for (const a of alias) { + this.addAlias(a, key); + } + } + } + getFieldFromAlias(alias) { + return this.aliases.get(alias); + } + buildAliasMap() { + for (const [key, value] of this.entries()) { + if (value.alias != null) { + this.addAlias(value.alias, key); + } + } + } +} +exports.PropertyMap = PropertyMap; +class SerdeOptions { + constructor(target, container = {}, properties = new PropertyMap(this)) { + Object.defineProperty(this, "target", { + enumerable: true, + configurable: true, + writable: true, + value: void 0 + }); + Object.defineProperty(this, "container", { + enumerable: true, + configurable: true, + writable: true, + value: void 0 + }); + Object.defineProperty(this, "properties", { + enumerable: true, + configurable: true, + writable: true, + value: void 0 + }); + this.target = target; + this.container = container; + this.properties = properties; + } + getClassName(stage, defaultName) { + var _a, _b, _c, _d; + if (defaultName === void 0) { defaultName = (_b = (_a = this.target) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.name; } + if ((0, utils_1.isString)(this.container.rename)) { + return this.container.rename; + } + else if (stage === exports.Stage.Serialize && (0, utils_1.isString)((_c = this.container.rename) === null || _c === void 0 ? void 0 : _c.serialize)) { + return this.container.rename.serialize; + } + else if (stage === exports.Stage.Deserialize && (0, utils_1.isString)((_d = this.container.rename) === null || _d === void 0 ? void 0 : _d.deserialize)) { + return this.container.rename.serialize; + } + else { + return defaultName; + } + } + getSerializedClassName(defaultName) { + var _a, _b; + if (defaultName === void 0) { defaultName = (_b = (_a = this.target) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.name; } + return this.getClassName(exports.Stage.Serialize, defaultName); + } + getDeserializedClassName(defaultName) { + var _a, _b; + if (defaultName === void 0) { defaultName = (_b = (_a = this.target) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.name; } + return this.getClassName(exports.Stage.Deserialize, defaultName); + } + getCaseConvention(stage) { + var _a, _b; + if ((0, utils_1.isNumber)(this.container.renameAll)) { + if (stage === exports.Stage.Serialize) { + return this.container.renameAll; + } + else { + return case_1.CaseConvention.CamelCase; + } + } + else if (stage === exports.Stage.Serialize && (0, utils_1.isNumber)((_a = this.container.renameAll) === null || _a === void 0 ? void 0 : _a.serialize)) { + return this.container.renameAll.serialize; + } + else if (stage === exports.Stage.Deserialize && (0, utils_1.isNumber)((_b = this.container.renameAll) === null || _b === void 0 ? void 0 : _b.deserialize)) { + return this.container.renameAll.deserialize; + } + } + applyCaseConvention(stage, property) { + const convention = this.getCaseConvention(stage); + return convention != null ? (0, case_1.convertCase)(property, convention) : property; + } + getPropertyRename(stage, property) { + var _a, _b; + const options = this.properties.get(property); + if (options == null) { + return; + } + else if ((0, utils_1.isString)(options.rename)) { + return options.rename; + } + else if (stage == exports.Stage.Serialize && (0, utils_1.isString)((_a = options.rename) === null || _a === void 0 ? void 0 : _a.serialize)) { + return options.rename.serialize; + } + else if (stage == exports.Stage.Deserialize && (0, utils_1.isString)((_b = options.rename) === null || _b === void 0 ? void 0 : _b.deserialize)) { + return options.rename.deserialize; + } + } + getSerializationPropertyName(property) { + return this.getPropertyRename(exports.Stage.Serialize, property) || + this.applyCaseConvention(exports.Stage.Serialize, property); + } + getNameFromAlias(alias) { + return this.properties.getFieldFromAlias(alias); + } + getDeserializationPropertyName(property) { + return this.getNameFromAlias(property) || + this.applyCaseConvention(exports.Stage.Deserialize, property); + } + shouldSkip(stage, property, value) { + var _a, _b, _c, _d; + const options = this.properties.get(property); + if (options == null) { + return false; + } + else if (typeof options.skip === 'boolean') { + return options.skip; + } + else if ((0, utils_1.isFunction)(options.skip)) { + return options.skip(value); + } + else if (stage === exports.Stage.Serialize && typeof ((_a = options.skip) === null || _a === void 0 ? void 0 : _a.serialize) === 'boolean') { + return options.skip.serialize; + } + else if (stage === exports.Stage.Serialize && (0, utils_1.isFunction)((_b = options.skip) === null || _b === void 0 ? void 0 : _b.serialize)) { + return options.skip.serialize(value); + } + else if (stage === exports.Stage.Deserialize && typeof ((_c = options.skip) === null || _c === void 0 ? void 0 : _c.deserialize) === 'boolean') { + return options.skip.deserialize; + } + else if (stage === exports.Stage.Deserialize && (0, utils_1.isFunction)((_d = options.skip) === null || _d === void 0 ? void 0 : _d.deserialize)) { + return options.skip.deserialize(value); + } + else { + return false; + } + } + shouldSerialize(property, value) { + return !this.shouldSkip(exports.Stage.Serialize, property, value); + } + shouldDeserialize(property, value) { + return !this.shouldSkip(exports.Stage.Deserialize, property, value); + } + defaultFor(property) { + const options = this.properties.get(property); + if (options != null && (0, utils_1.isFunction)(options.default)) { + return options.default(); + } + else if ((0, utils_1.isFunction)(this.container.default)) { + return this.container.default(); + } + } +} +exports.SerdeOptions = SerdeOptions; diff --git a/dist/registry.d.ts b/dist/registry.d.ts index 9037a58..f8643b7 100644 --- a/dist/registry.d.ts +++ b/dist/registry.d.ts @@ -2,10 +2,10 @@ import { Deserialize } from './de'; import { Serialize } from './ser'; export declare class Registry { serializers: Map>; - deserializers: Map; + deserializers: Map>; registerSerializer(ctor: Function, serialize: Serialize): void; - registerDeserializer(ctor: Function, deserialize: Deserialize): void; + registerDeserializer(ctor: Function, deserialize: Deserialize): void; } export declare const GlobalRegistry: Registry; export declare const registerSerializer: (ctor: Function, serialize: Serialize) => void; -export declare const registerDeserializer: (ctor: Function, deserialize: Deserialize) => void; +export declare const registerDeserializer: (ctor: Function, deserialize: Deserialize) => void; diff --git a/dist/ser/impl.d.ts b/dist/ser/impl.d.ts index 985e586..d214d60 100644 --- a/dist/ser/impl.d.ts +++ b/dist/ser/impl.d.ts @@ -1,4 +1,4 @@ -import { SerdeOptions } from './decorator'; import { Serializer } from './interface'; import { Nullable } from '../utils'; +import { SerdeOptions } from '../option'; export declare function serialize>(serializer: S, value: V, optionsGetter?: (value: V) => Nullable): T; diff --git a/dist/ser/impl.js b/dist/ser/impl.js index d6fb39c..c44dfca 100644 --- a/dist/ser/impl.js +++ b/dist/ser/impl.js @@ -9,7 +9,8 @@ class UnhandledTypeError extends TypeError { } function serializeObject(serializer, obj, options) { for (const key in obj) { - serializer.serializeEntry(key, obj[key]); + const name = (options === null || options === void 0 ? void 0 : options.getSerializationPropertyName(key)) || key; + serializer.serializeEntry(name, obj[key]); } return serializer.end(); } @@ -20,7 +21,7 @@ function serializeClass(serializer, value, options) { } const defaultGetter = (value) => { var _a; - return (_a = value[Symbol.metadata]) === null || _a === void 0 ? void 0 : _a.serde; + return (_a = value.constructor[Symbol.metadata]) === null || _a === void 0 ? void 0 : _a.serde; }; function serialize(serializer, value, optionsGetter = defaultGetter) { switch (typeof value) { diff --git a/dist/utils.d.ts b/dist/utils.d.ts index d5dfc33..4e4591e 100644 --- a/dist/utils.d.ts +++ b/dist/utils.d.ts @@ -6,7 +6,6 @@ export type Primitive = string | number | boolean | symbol | bigint | null | und export interface ToString { toString(): string; } -export declare function staticImplements(): (constructor: U) => void; export declare function isPlainObject(value: any): boolean; export declare function isFunction(value: any): value is Function; export declare function isIterable(value: any): value is Iterable; diff --git a/dist/utils.js b/dist/utils.js index 8dbf243..ed4dbbc 100644 --- a/dist/utils.js +++ b/dist/utils.js @@ -1,15 +1,11 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.IterResult = void 0; -exports.staticImplements = staticImplements; exports.isPlainObject = isPlainObject; exports.isFunction = isFunction; exports.isIterable = isIterable; exports.isString = isString; exports.isNumber = isNumber; -function staticImplements() { - return (constructor) => { constructor; }; -} function isPlainObject(value) { return Object.getPrototypeOf(value) === Object.prototype; } diff --git a/src/de/forward.ts b/src/de/forward.ts index 6ba63bf..d535dcd 100644 --- a/src/de/forward.ts +++ b/src/de/forward.ts @@ -2,16 +2,16 @@ import { IterResult } from '../utils' import { Deserialize, IDeserializer, IterableAccess, IVisitor, MapAccess } from './interface' export class ForwardMapAccess extends MapAccess { - private readonly keys: string[] - private readonly values: any[] + private readonly _keys: string[] + private readonly _values: any[] private kindex: number = 0 private vindex: number = 0 constructor(keys: string[], values: any[]) { super() - this.keys = keys - this.values = values + this._keys = keys + this._values = values } static fromObject(obj: object): ForwardMapAccess { @@ -21,17 +21,17 @@ export class ForwardMapAccess extends MapAccess { ) } - nextKeySeed(_seed: K): IteratorResult { + nextKeySeed>(_seed: K): IteratorResult { return this.nextKey() } - nextValueSeed(_seed: V): IteratorResult { + nextValueSeed>(_seed: V): IteratorResult { return this.nextValue() } nextKey(): IteratorResult { if (this.kindex < this.keys.length) { - return IterResult.Next(this.keys[this.kindex++]) as IteratorResult + return IterResult.Next(this._keys[this.kindex++]) as IteratorResult } else { return IterResult.Done() } @@ -39,7 +39,7 @@ export class ForwardMapAccess extends MapAccess { nextValue(): IteratorResult { if (this.vindex < this.values.length) { - return IterResult.Next(this.values[this.vindex++]) as IteratorResult + return IterResult.Next(this._values[this.vindex++]) as IteratorResult } else { return IterResult.Done() } diff --git a/src/de/generic.ts b/src/de/generic.ts index 0ddd333..baff64e 100644 --- a/src/de/generic.ts +++ b/src/de/generic.ts @@ -1,13 +1,15 @@ -import { IDeserializer, IIterableAccess, IMapAccess, IVisitor } from './interface' +import { SerdeOptions } from '../option' +import { isFunction, isString, IterResult, Nullable } from '../utils' +import { Deserialize, IDeserializer, IIterableAccess, IMapAccess, IVisitor } from './interface' export class GenericSeed { readonly visitor: IVisitor - constructor(visitor: IVisitor = new GenericVisitor()) { + constructor(visitor: IVisitor = new ProxyVisitor()) { this.visitor = visitor } - static deserialize(deserializer: D, visitor: IVisitor = new GenericVisitor()): T { + static deserialize(deserializer: D, visitor: IVisitor = new ProxyVisitor()): T { return deserializer.deserializeAny(visitor) } @@ -16,36 +18,150 @@ export class GenericSeed { } } -export class GenericVisitor implements IVisitor { +class ProxyMapAccess implements IMapAccess { + private access!: IMapAccess + private options?: SerdeOptions + + constructor(access: IMapAccess, options?: SerdeOptions) { + if (access instanceof ProxyMapAccess) { + return this + } + + this.access = access + this.options = options + } + + private wrapResponse = IteratorResult>(result: I): I { + if (result.done) { + return result + } else if (isString(result.value)) { + const key = this.options?.getDeserializationPropertyName(result.value) ?? result.value + return IterResult.Next(key) as I + } else if (Array.isArray(result.value)) { + const [alias, value] = result.value + const key = this.options?.getDeserializationPropertyName(alias) ?? alias + return IterResult.Next([ + key, + value + ]) as I + } else { + return result + } + } + + nextKeySeed>(seed: K): IteratorResult { + return this.wrapResponse(this.access.nextKeySeed(seed)) + } + + nextValueSeed>(seed: V): IteratorResult { + return this.access.nextValueSeed(seed) + } + + nextEntrySeed, V extends Deserialize>(kseed: K, vseed: V): IteratorResult<[TK, TV]> { + return this.wrapResponse<[TK, TV]>(this.access.nextEntrySeed(kseed, vseed)) + } + + nextKey(): IteratorResult { + return this.wrapResponse(this.access.nextKey()) + } + + nextValue(): IteratorResult { + return this.access.nextValue() + } + + nextEntry(): IteratorResult<[K, V]> { + return this.wrapResponse<[K, V]>(this.access.nextEntry()) + } + + sizeHint?(): Nullable { + return this.access.sizeHint?.() + } + + private *generate(next: () => IteratorResult): Iterator { + let item: IteratorResult + + while ((item = next()) && !item.done) { + yield item.value + } + } + + keys>(seed?: K): Iterator { + return this.generate( + seed == null ? + this.nextKey.bind(this) : + this.nextKeySeed.bind(this, seed) as any + ) + } + + values>(seed?: V): Iterator { + return this.generate( + seed == null ? + this.nextValue.bind(this) : + this.nextValueSeed.bind(this, seed) as any + ) + } + + entries, V extends Deserialize>(kseed?: K, vseed?: V): Iterator<[TK, TV]> { + return this.generate( + kseed == null || vseed == null ? + this.nextEntry.bind(this) : + this.nextEntrySeed.bind(this, kseed, vseed) as any + ) + } + + [Symbol.iterator](): Iterator<[K, V]> { + return this.entries() as Iterator<[K, V]> + } +} + +export class ProxyVisitor implements IVisitor { + private overrides?: Partial> + private options?: SerdeOptions + + constructor(overrides?: Partial>, options?: SerdeOptions) { + if (overrides instanceof ProxyVisitor) { + return overrides + } + + this.overrides = overrides + this.options = options + } + visitBoolean(value: boolean): T { - return value as T + return this.overrides?.visitBoolean?.(value) ?? value as T } visitNumber(value: number): T { - return value as T + return this.overrides?.visitNumber?.(value) ?? value as T } visitBigInt(value: bigint): T { - return value as T + return this.overrides?.visitBigInt?.(value) ?? value as T } visitString(value: string): T { - return value as T + return this.overrides?.visitString?.(value) ?? value as T } visitSymbol(value: symbol): T { - return value as T + return this.overrides?.visitSymbol?.(value) ?? value as T } visitNull(): T { - return null as T + return this.overrides?.visitNull?.() ?? null as T } visitObject(access: IMapAccess): T { + const proxy = new ProxyMapAccess(access, this.options) + + if (isFunction(this.overrides?.visitObject)) { + return this.overrides?.visitObject(proxy) + } + const result = [] let entry - while ((entry = access.nextEntry()) && !entry.done) { + while ((entry = proxy.nextEntry()) && !entry.done) { result.push(entry.value) } @@ -53,6 +169,10 @@ export class GenericVisitor implements IVisitor { } visitIterable(access: IIterableAccess): T { + if (isFunction(this.overrides?.visitIterable)) { + return this.overrides?.visitIterable(access) + } + const result = [] let element @@ -64,5 +184,53 @@ export class GenericVisitor implements IVisitor { } } +export class ProxyDeserializer implements IDeserializer { + private readonly deserializer: IDeserializer + private readonly options?: SerdeOptions + constructor(deserializer: IDeserializer, options?: SerdeOptions) { + this.deserializer = deserializer + this.options = options + } + + deserializeAny>(visitor: V): T { + return this.deserializer.deserializeAny(new ProxyVisitor(visitor, this.options)) + } + + deserializeBoolean>(visitor: V): T { + return this.deserializer.deserializeBoolean(new ProxyVisitor(visitor, this.options)) + } + + deserializeNumber>(visitor: V): T { + return this.deserializer.deserializeNumber(new ProxyVisitor(visitor, this.options)) + } + + deserializeBigInt>(visitor: V): T { + return this.deserializer.deserializeBigInt(new ProxyVisitor(visitor, this.options)) + } + + deserializeString>(visitor: V): T { + return this.deserializer.deserializeString(new ProxyVisitor(visitor, this.options)) + } + + deserializeSymbol>(visitor: V): T { + return this.deserializer.deserializeSymbol(new ProxyVisitor(visitor, this.options)) + } + + deserializeNull>(visitor: V): T { + return this.deserializer.deserializeNull(new ProxyVisitor(visitor, this.options)) + } + + deserializeObject>(visitor: V): T { + return this.deserializer.deserializeObject(new ProxyVisitor(visitor, this.options)) + } + + deserializeIterable>(visitor: V): T { + return this.deserializer.deserializeIterable(new ProxyVisitor(visitor, this.options)) + } + + deserializeFunction>(visitor: V): T { + return this.deserializer.deserializeFunction(new ProxyVisitor(visitor, this.options)) + } +} diff --git a/src/de/impl.ts b/src/de/impl.ts index fc65614..287e1b1 100644 --- a/src/de/impl.ts +++ b/src/de/impl.ts @@ -1,5 +1,6 @@ import { GlobalRegistry, Registry } from '../registry' import { Constructor } from '../utils' +import { ProxyDeserializer } from './generic' import { IDeserializer } from './interface' export function deserialize(deserializer: D, into: Constructor, registry: Registry = GlobalRegistry): T { @@ -8,7 +9,7 @@ export function deserialize(deserializer: D, into: C if (de == null) { throw new ReferenceError(`No deserializer for ${into.name}`) } else { - return de(deserializer) + return de(new ProxyDeserializer(deserializer, (into as any)?.[Symbol.metadata]?.serde)) } } diff --git a/src/de/interface.ts b/src/de/interface.ts index 934e803..a574891 100644 --- a/src/de/interface.ts +++ b/src/de/interface.ts @@ -1,20 +1,24 @@ -import { IterResult, Nullable } from "../utils" +import { IterResult, Nullable } from '../utils' export interface IMapAccess { - nextKeySeed(seed: K): IteratorResult - nextValueSeed(seed: V): IteratorResult - nextEntrySeed(kseed: K, vseed: V): IteratorResult<[TK, TV]> + nextKeySeed>(seed: K): IteratorResult + nextValueSeed>(seed: V): IteratorResult + nextEntrySeed, V extends Deserialize>(kseed: K, vseed: V): IteratorResult<[TK, TV]> nextKey(): IteratorResult nextValue(): IteratorResult nextEntry(): IteratorResult<[K, V]> sizeHint?(): Nullable + entries>(seed?: K): Iterator + values>(seed?: V): Iterator + entries, V extends Deserialize>(kseed: K, vseed: V): Iterator<[TK, TV]> + [Symbol.iterator](): Iterator<[K, V]> } export abstract class MapAccess { - abstract nextKeySeed(seed: K): IteratorResult - abstract nextValueSeed(seed: V): IteratorResult + abstract nextKeySeed>(seed: K): IteratorResult + abstract nextValueSeed>(seed: V): IteratorResult - nextEntrySeed(kseed: K, vseed: V): IteratorResult<[TK, TV]> { + nextEntrySeed, V extends Deserialize>(kseed: K, vseed: V): IteratorResult<[TK, TV]> { const key = this.nextKeySeed(kseed) as IteratorResult if (!key.done) { @@ -44,6 +48,43 @@ export abstract class MapAccess { return IterResult.Done() } + + private *generate(next: () => IteratorResult): Iterator { + let item + + while ((item = next())) { + yield item.value + } + } + + + keys>(seed?: K): Iterator { + return this.generate( + seed == null ? + this.nextKey.bind(this) : + this.nextKeySeed.bind(this, seed) as any + ) + } + + values>(seed?: V): Iterator { + return this.generate( + seed == null ? + this.nextValue.bind(this) : + this.nextValueSeed.bind(this, seed) as any + ) + } + + entries, V extends Deserialize>(kseed?: K, vseed?: V): Iterator<[TK, TV]> { + return this.generate( + kseed == null || vseed == null ? + this.nextEntry.bind(this) : + this.nextEntrySeed.bind(this, kseed, vseed) as any + ) + } + + [Symbol.iterator](): Iterator { + return this.entries() as Iterator + } } export interface IIterableAccess { @@ -67,19 +108,19 @@ export interface IVisitor { } export interface IDeserializer { - deserializeAny>(visitor: V): T - deserializeBoolean>(visitor: V): T - deserializeNumber>(visitor: V): T - deserializeBigInt>(visitor: V): T - deserializeString>(visitor: V): T - deserializeSymbol>(visitor: V): T - deserializeNull>(visitor: V): T - deserializeObject>(visitor: V): T - deserializeIterable>(visitor: V): T - deserializeFunction>(visitor: V): T + deserializeAny(visitor: Partial>): T + deserializeBoolean(visitor: Partial>): T + deserializeNumber(visitor: Partial>): T + deserializeBigInt(visitor: Partial>): T + deserializeString(visitor: Partial>): T + deserializeSymbol(visitor: Partial>): T + deserializeNull(visitor: Partial>): T + deserializeObject(visitor: Partial>): T + deserializeIterable(visitor: Partial>): T + deserializeFunction(visitor: Partial>): T } -export interface Deserialize { - (deserializer: IDeserializer): T +export interface Deserialize { + (deserializer: IDeserializer): T } diff --git a/src/decorator.ts b/src/decorator.ts new file mode 100644 index 0000000..23b858d --- /dev/null +++ b/src/decorator.ts @@ -0,0 +1,44 @@ +import { ContainerOptions, PropertyOptions, SerdeOptions } from './option' +import { Nullable } from './utils' + +const ContextKind = Object.freeze({ + Class: 'class', + Method: 'method', + Getter: 'getter', + Setter: 'setter', + Field: 'field', + Accessor: 'accessor' +} as const) + +function decorateClass(target: Function, context: DecoratorContext, options: ContainerOptions) { + const serde = context.metadata.serde as Nullable + + context.metadata.serde = new SerdeOptions( + target, + options, + serde?.properties + ) +} + +function decorateField(context: DecoratorContext, options: PropertyOptions) { + const serde = (context.metadata.serde as SerdeOptions) || new SerdeOptions(undefined) + serde.properties.set(context.name as string, options) + context.metadata.serde = serde +} + +export function serde(options: ContainerOptions | PropertyOptions) { + return function(target: any, context: DecoratorContext) { + switch (context.kind) { + case ContextKind.Class: + decorateClass(target, context, options) + break + + case ContextKind.Field: + decorateField(context, options) + + default: + break + } + } +} + diff --git a/src/index.ts b/src/index.ts index 9dd5a0f..6b0679e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,8 @@ import '@tsmetadata/polyfill' export * as ser from './ser/impl' export * as de from './de/impl' +export * from './decorator' +export * from './option' export * from './case' export * from './registry' export * from './utils' diff --git a/src/ser/decorator.ts b/src/option.ts similarity index 60% rename from src/ser/decorator.ts rename to src/option.ts index ef173b3..a4ae895 100644 --- a/src/ser/decorator.ts +++ b/src/option.ts @@ -1,7 +1,7 @@ -import { CaseConvention, convertCase } from '../case' -import { Deserialize } from '../de' -import { isFunction, isNumber, isString, Nullable } from '../utils' -import { Serialize } from './interface' +import { Deserialize } from './de' +import { Serialize } from './ser' +import { CaseConvention, convertCase } from './case' +import { isFunction, isIterable, isNumber, isString, Nullable } from './utils' export interface RenameOptions { serialize?: string @@ -37,7 +37,7 @@ export interface PropertyOptions { rename?: RenameOptions | string skip?: SkipOptions | Predicate | boolean serializeWith?: Serialize - deserializeWith?: Deserialize + deserializeWith?: Deserialize } export const Stage = Object.freeze({ @@ -45,14 +45,67 @@ export const Stage = Object.freeze({ Deserialize: 1 } as const) +export class PropertyMap extends Map { + private readonly options: SerdeOptions + private readonly aliases: Map = new Map() + + constructor(options: SerdeOptions, iterable?: Iterable) { + super(iterable) + this.options = options + this.buildAliasMap() + } + + private setAliasesFromOptions(key: string, options: PropertyOptions) { + this.aliases.set(key, key) + + if (options.alias != null) { + this.addAlias(options.alias, key) + } + + if (isString(options.rename)) { + this.aliases.set(options.rename, key) + } else if (isNumber(this.options.container.renameAll)) { + this.aliases.set(this.options.applyCaseConvention(Stage.Deserialize, key), key) + } + } + + set(key: string, value: PropertyOptions): this { + super.set(key, value) + this.setAliasesFromOptions(key, value) + return this + } + + addAlias(alias: string | Iterable, key: string) { + if (isString(alias)) { + this.aliases.set(alias, key) + } else if (isIterable(alias)) { + for (const a of alias) { + this.addAlias(a, key) + } + } + } + + getFieldFromAlias(alias: string) { + return this.aliases.get(alias) + } + + private buildAliasMap() { + for (const [key, value] of this.entries()) { + if (value.alias != null) { + this.addAlias(value.alias, key) + } + } + } +} + export type Stage = typeof Stage[keyof typeof Stage] export class SerdeOptions { readonly target: Nullable readonly container: ContainerOptions - readonly properties: Map + readonly properties: PropertyMap - constructor(target: any, container: ContainerOptions = {}, properties: Map = new Map()) { + constructor(target: any, container: ContainerOptions = {}, properties: PropertyMap = new PropertyMap(this)) { this.target = target this.container = container this.properties = properties @@ -78,45 +131,54 @@ export class SerdeOptions { return this.getClassName(Stage.Deserialize, defaultName) } - private applyPropertyCase(stage: Stage, property: string) { + getCaseConvention(stage: Stage) { if (isNumber(this.container.renameAll)) { - return convertCase(property, this.container.renameAll) + if (stage === Stage.Serialize) { + return this.container.renameAll + } else { + return CaseConvention.CamelCase + } } else if (stage === Stage.Serialize && isNumber(this.container.renameAll?.serialize)) { - return convertCase(property, this.container.renameAll.serialize) + return this.container.renameAll.serialize } else if (stage === Stage.Deserialize && isNumber(this.container.renameAll?.deserialize)) { - return convertCase(property, this.container.renameAll.deserialize) - } else { - return property + return this.container.renameAll.deserialize } } - private getPropertyName(stage: Stage, property: string) { + applyCaseConvention(stage: Stage, property: string) { + const convention = this.getCaseConvention(stage) + return convention != null ? convertCase(property, convention) : property + } + + getPropertyRename(stage: Stage, property: string) { const options = this.properties.get(property) if (options == null) { - return property + return } else if (isString(options.rename)) { return options.rename } else if (stage == Stage.Serialize && isString(options.rename?.serialize)) { return options.rename.serialize } else if (stage == Stage.Deserialize && isString(options.rename?.deserialize)) { return options.rename.deserialize - } else { - return property } } getSerializationPropertyName(property: string) { - const name = this.getPropertyName(Stage.Serialize, property) - return this.applyPropertyCase(Stage.Serialize, name) + return this.getPropertyRename(Stage.Serialize, property) || + this.applyCaseConvention(Stage.Serialize, property) + } + + getNameFromAlias(alias: string) { + return this.properties.getFieldFromAlias(alias) } getDeserializationPropertyName(property: string) { - const name = this.getPropertyName(Stage.Deserialize, property) - return this.applyPropertyCase(Stage.Deserialize, name) + return this.getNameFromAlias(property) || + this.applyCaseConvention(Stage.Deserialize, property) } - private shouldSkip(stage: Stage, property: string, value: any) { + shouldSkip(stage: Stage, property: string, value: any) { const options = this.properties.get(property) if (options == null) { return false diff --git a/src/registry.ts b/src/registry.ts index 8530f84..fd99e3d 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -3,13 +3,13 @@ import { Serialize } from './ser' export class Registry { serializers: Map> = new Map() - deserializers: Map = new Map() + deserializers: Map> = new Map() registerSerializer(ctor: Function, serialize: Serialize) { this.serializers.set(ctor, serialize) } - registerDeserializer(ctor: Function, deserialize: Deserialize) { + registerDeserializer(ctor: Function, deserialize: Deserialize) { this.deserializers.set(ctor, deserialize) } } diff --git a/src/ser/impl.ts b/src/ser/impl.ts index 0c3545b..0541d9c 100644 --- a/src/ser/impl.ts +++ b/src/ser/impl.ts @@ -1,6 +1,6 @@ -import { SerdeOptions } from './decorator' import { ISerializeObject, Serializer } from './interface' import { isPlainObject, Nullable } from '../utils' +import { SerdeOptions } from '../option' class UnhandledTypeError extends TypeError { constructor(serializer: Serializer, value: any) { @@ -10,7 +10,8 @@ class UnhandledTypeError extends TypeError { function serializeObject>(serializer: S, obj: V, options?: SerdeOptions): T { for (const key in obj) { - serializer.serializeEntry(key, obj[key]) + const name = options?.getSerializationPropertyName(key) || key + serializer.serializeEntry(name, obj[key]) } return serializer.end() @@ -23,7 +24,7 @@ function serializeClass>(serializer } const defaultGetter = (value: any): Nullable => { - return value[Symbol.metadata]?.serde + return value.constructor[Symbol.metadata]?.serde } export function serialize>(serializer: S, value: V, optionsGetter: (value: V) => Nullable = defaultGetter): T { diff --git a/src/utils.ts b/src/utils.ts index cc8f44a..23e8b06 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,10 +10,6 @@ export interface ToString { toString(): string } -export function staticImplements() { - return (constructor: U) => { constructor } -} - export function isPlainObject(value: any): boolean { return Object.getPrototypeOf(value) === Object.prototype }