diff --git a/src/de/interface.ts b/src/de/interface.ts index be234a0..90cd7db 100644 --- a/src/de/interface.ts +++ b/src/de/interface.ts @@ -16,10 +16,10 @@ export abstract class DefaultMapAccessImpl implements MapAccess { nextEntrySeed, V extends Deserialize>(kseed: K, vseed: V): Nullable<[TK, TV]> { const key = this.nextKeySeed(kseed) as Nullable - if (key) { + if (key !== undefined) { const value = this.nextValueSeed(vseed) as Nullable - if (value) { + if (value !== undefined) { return [key, value] } } diff --git a/src/de/mixin.ts b/src/de/mixin.ts index 37d5873..0a4b844 100644 --- a/src/de/mixin.ts +++ b/src/de/mixin.ts @@ -1,16 +1,42 @@ -import { Serde } from '../decorator' +import { getMetadata, Serde } from '../decorator' +import { SerdeOptions, Stage } from '../options' import { Constructor, staticImplements } from '../utils' import { GenericVisitor } from './generic' import { Deserialize, Deserializer } from './interface' +type DeserializeConstructor = Deserialize & { new(): Deserialize } + +function deserializeWith>(deserializer: D, into: E, options: SerdeOptions): T { + const visitor = new GenericVisitor() + const obj = deserializer.deserializeObject(visitor) as any + const target = new into() + const newObject = {} as any + + for (const property in target) { + const name = options.getPropertyName(property, Stage.Deserialize) + const value = obj[name] || options.getDefault(property) + newObject[property] = value + delete obj[name] + } + + if (options.options.denyUnknownFields && Object.keys(obj).length > 0) { + throw new TypeError(`Unexpected fields: ${Object.keys(obj).join(', ')}`) + } + + return Object.assign(target, newObject) +} + export function deserialize(constructor: C) { @staticImplements>() class Deserializable extends constructor { + static name = constructor.name + static deserialize(deserializer: D): T { - const visitor = new GenericVisitor() - return deserializer.deserializeAny(visitor) + return deserializeWith(deserializer, this, getMetadata(this)) } } + + // @ts-ignore Deserializable[Serde] = (constructor as any)[Serde] return Deserializable diff --git a/src/decorator.ts b/src/decorator.ts index 8137719..9a2fdca 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -1,4 +1,4 @@ -import { ContainerOptions, PropertyOptions, SerdeOptions } from "./options" +import { ContainerOptions, PropertyOptions, SerdeOptions } from './options' export const Serde = Symbol('Serde') @@ -38,3 +38,7 @@ export function serde(options: ContainerOptions | PropertyOptions) { } } } + +export function getMetadata(value: any) { + return value[Serde] +} diff --git a/src/json.ts b/src/json.ts index aadc28a..0968a84 100644 --- a/src/json.ts +++ b/src/json.ts @@ -244,10 +244,10 @@ class JSONObjectSerializer implements ObjectSerializer { } serializeWith(this.ser, key) + this.ser.write(':') } serializeValue(value: U): void { - this.ser.write(':') serializeWith(this.ser, value) } diff --git a/src/options.ts b/src/options.ts index e946980..370c09c 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,5 +1,7 @@ import { CaseConvention, convertCase } from './case' -import { isNumber, isString, Morphism } from './utils' +import { Deserializer } from './de' +import { Serializer } from './ser' +import { isFunction, isNumber, isString, Morphism, Nullable } from './utils' export interface RenameOptions { @@ -14,7 +16,7 @@ export interface RenameAllOptions { export interface ContainerOptions { // deserialization only - default?: () => T + default?: () => any rename?: RenameOptions | string renameAll?: RenameAllOptions | CaseConvention // deserialization only @@ -34,12 +36,17 @@ export interface SkipOptions { deserializing?: ConditionalSkipOptions | boolean } +export type CustomSerializer = >(value: V, serializer: S) => T +export type CustomDeserializer = (deserializer: D) => T + export interface PropertyOptions { alias?: string + // deserialization only + default?: () => any flatten?: boolean rename?: RenameOptions | string - serializeWith?: Morphism - deserializeWith: Morphism + //serializeWith?: CustomSerializer + //deserializeWith: CustomDeserializer skip?: SkipOptions | boolean } @@ -108,4 +115,41 @@ export class SerdeOptions { const name = options != null ? this.getPropertyRename(property, stage, options) : property return this.getPropertyCase(name, stage) } + + getSerializationName(property: string) { + return this.getPropertyName(property, Stage.Serialize) + } + + getDeserializationName(property: string) { + return this.getPropertyName(property, Stage.Deserialize) + } + + getDefault(property: string) { + const options = this.properties.get(property) + if (options && isFunction(options.default)) { + return options.default() + } else if (isFunction(this.options.default)) { + return this.options.default() + } + } + + getCustomImpl(property: string, stage: Stage) { + const options = this.properties.get(property) + if (options != null) { + if (stage === Stage.Serialize && isFunction(options.serializeWith)) { + return options.serializeWith + } else if (stage === Stage.Deserialize && isFunction(options.deserializeWith)) { + return options.deserializeWith + } + } + } + + getSerializer(property: string): Nullable { + return this.getCustomImpl(property, Stage.Serialize) as CustomSerializer + } + + getDeserializer(property: string): Nullable { + return this.getCustomImpl(property, Stage.Deserialize) as CustomDeserializer + } } + diff --git a/src/ser/impl.ts b/src/ser/impl.ts index 64fa155..e3d4b94 100644 --- a/src/ser/impl.ts +++ b/src/ser/impl.ts @@ -1,23 +1,25 @@ import { Serde } from '../decorator' import { SerdeOptions, Stage } from '../options' import { ifNull, isFunction, isIterable, Nullable, orElse } from '../utils' -import { IterableSerializer, ObjectSerializer, Serializable, Serializer } from './interface' +import { IterableSerializer, Serializable, Serializer } from './interface' const unhandledType = (serializer: any, value: any) => new TypeError(`'${serializer.constructor.name}' has no method for value type '${typeof value}'`) -function serializeEntries>(serializer: ObjectSerializer, value: E, options?: SerdeOptions) { +function serializeEntries>(serializer: Serializer, value: E, options?: SerdeOptions) { let state + const objectSerializer = serializer.serializeObject!() + for (const [key, val] of value) { const name = options?.getPropertyName(key as string, Stage.Serialize) ?? key - state = serializer.serializeKey(name) - state = serializer.serializeValue(val) + state = objectSerializer.serializeKey(name) + state = objectSerializer.serializeValue(val) } - return serializer.end() + return objectSerializer.end() } -function serializeObject>(serializer: ObjectSerializer, value: R, options?: SerdeOptions) { +function serializeObject>(serializer: Serializer, value: R, options?: SerdeOptions) { return serializeEntries(serializer, Object.entries(value) as Iterable<[K, V]>, options) } @@ -58,7 +60,7 @@ export function serializeWith(serializer: Serializer, value: Serializable, if (isIterable(value) && isFunction(serializer.serializeIterable)) { return serializeIter(serializer.serializeIterable(), value) } else if (isFunction(serializer.serializeObject)) { - return serializeObject(serializer.serializeObject!(), value as Record, optionsGetter(value)) + return serializeObject(serializer, value as Record, optionsGetter(value)) } // deliberate fallthrough when the above fail default: return serializeAny(value)