refactor structure; custom json serializer
This commit is contained in:
parent
7116abb2ee
commit
b2f19f1e0c
12 changed files with 428 additions and 144 deletions
63
src/case.ts
Normal file
63
src/case.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
export const CaseConvention = Object.freeze({
|
||||
Lowercase: 0,
|
||||
Uppercase: 1,
|
||||
PascalCase: 2,
|
||||
CamelCase: 3,
|
||||
SnakeCase: 4,
|
||||
ScreamingSnakeCase: 5,
|
||||
KebabCase: 6,
|
||||
ScreamingKebabCase: 7
|
||||
} as const)
|
||||
|
||||
export type CaseConvention = typeof CaseConvention[keyof typeof CaseConvention]
|
||||
|
||||
const wordBoundaryRegex = /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g
|
||||
|
||||
function identifyWords(value: string) {
|
||||
return value && value.match(wordBoundaryRegex)
|
||||
}
|
||||
|
||||
const lower = (ch: string) => ch.toLowerCase()
|
||||
const upper = (ch: string) => ch.toUpperCase()
|
||||
|
||||
const first = <T>(xs: T[] | string) => xs && xs[0]
|
||||
const tail = <T>(xs: T[] | string) => xs && xs.slice(1)
|
||||
|
||||
function upperFirst(xs: string) {
|
||||
return upper(first(xs)) + tail(xs)
|
||||
}
|
||||
|
||||
function toPascalCase(words: string[]) {
|
||||
return words.map(lower).map(upperFirst).join('')
|
||||
}
|
||||
|
||||
const joinMap = (fn: (x: string) => string, delim: string, xs: string[]) => {
|
||||
return xs.map(fn).join(delim)
|
||||
}
|
||||
|
||||
export function convertCase(value: string, convention: CaseConvention) {
|
||||
const words = identifyWords(value)
|
||||
if (!words || words.length <= 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
switch (convention) {
|
||||
case CaseConvention.Lowercase:
|
||||
return words.join('').toLowerCase()
|
||||
case CaseConvention.Uppercase:
|
||||
return words.join('').toUpperCase()
|
||||
case CaseConvention.PascalCase:
|
||||
return toPascalCase(words)
|
||||
case CaseConvention.CamelCase:
|
||||
const pascal = toPascalCase(words)
|
||||
return first<string>(pascal).toLowerCase() + tail(pascal)
|
||||
case CaseConvention.SnakeCase:
|
||||
return joinMap(lower, '_', words)
|
||||
case CaseConvention.ScreamingSnakeCase:
|
||||
return joinMap(upper, '_', words)
|
||||
case CaseConvention.KebabCase:
|
||||
return joinMap(lower, '-', words)
|
||||
case CaseConvention.ScreamingKebabCase:
|
||||
return joinMap(upper, '-', words)
|
||||
}
|
||||
}
|
|
@ -48,21 +48,6 @@ export class GenericVisitor<T> implements Visitor<T> {
|
|||
return result
|
||||
}
|
||||
|
||||
visitFunction?(value: Function): T {
|
||||
return value as T
|
||||
}
|
||||
|
||||
visitMap?(access: MapAccess): T {
|
||||
const result = new Map()
|
||||
let entry
|
||||
|
||||
while ((entry = access.nextEntry<string, any>())) {
|
||||
result.set(entry[0], entry[1])
|
||||
}
|
||||
|
||||
return result as T
|
||||
}
|
||||
|
||||
visitIterable?(access: IterableAccess): T {
|
||||
const result = new Array(access.sizeHint())
|
||||
let element
|
||||
|
@ -73,9 +58,5 @@ export class GenericVisitor<T> implements Visitor<T> {
|
|||
|
||||
return result as T
|
||||
}
|
||||
|
||||
visitClass?(_name: string, _fields: string[], _value: any): T {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -62,10 +62,7 @@ export interface Visitor<T> {
|
|||
visitSymbol(value: symbol): T
|
||||
visitNull(): T
|
||||
visitObject(value: MapAccess): T
|
||||
visitFunction?(value: Function): T
|
||||
visitMap?(value: MapAccess): T
|
||||
visitIterable?(value: IterableAccess): T
|
||||
visitClass?(name: string, fields: string[], value: any): T
|
||||
}
|
||||
|
||||
export interface Deserializer {
|
||||
|
@ -77,10 +74,7 @@ export interface Deserializer {
|
|||
deserializeSymbol<T, V extends Visitor<T>>(visitor: V): T
|
||||
deserializeNull<T, V extends Visitor<T>>(visitor: V): T
|
||||
deserializeObject<T, V extends Visitor<T>>(visitor: V): T
|
||||
deserializeFunction?<T, V extends Visitor<T>>(visitor: V): T
|
||||
deserializeMap?<T, V extends Visitor<T>>(visitor: V): T
|
||||
deserializeIterable?<T, V extends Visitor<T>>(visitor: V): T
|
||||
deserializeClass?<T, V extends Visitor<T>>(name: string, fields: string[], visitor: V): T
|
||||
}
|
||||
|
||||
export interface Deserialize<T> {
|
||||
|
|
|
@ -1,30 +1,16 @@
|
|||
import { CaseConvention, Constructor, staticImplements } from '../utils'
|
||||
import { Constructor, staticImplements } from '../utils'
|
||||
import { GenericVisitor } from './generic'
|
||||
import { Deserialize, Deserializer } from './interface'
|
||||
|
||||
export interface DeserializationOptions {
|
||||
rename?: string
|
||||
renameAll?: CaseConvention
|
||||
}
|
||||
|
||||
const DefaultDeserializationOptions = {}
|
||||
|
||||
export function deserialize(options?: DeserializationOptions) {
|
||||
options = {
|
||||
...DefaultDeserializationOptions,
|
||||
...options
|
||||
}
|
||||
|
||||
return function <T, C extends Constructor>(constructor: C) {
|
||||
@staticImplements<Deserialize<T>>()
|
||||
class Deserializable extends constructor {
|
||||
static deserialize<D extends Deserializer>(deserializer: D): T {
|
||||
const visitor = new GenericVisitor<T>()
|
||||
return deserializer.deserializeAny(visitor)
|
||||
}
|
||||
export function deserialize<T, C extends Constructor>(constructor: C) {
|
||||
@staticImplements<Deserialize<T>>()
|
||||
class Deserializable extends constructor {
|
||||
static deserialize<D extends Deserializer>(deserializer: D): T {
|
||||
const visitor = new GenericVisitor<T>()
|
||||
return deserializer.deserializeAny(visitor)
|
||||
}
|
||||
|
||||
return Deserializable
|
||||
}
|
||||
|
||||
return Deserializable
|
||||
}
|
||||
|
||||
|
|
40
src/decorator.ts
Normal file
40
src/decorator.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { ContainerOptions, PropertyOptions, SerdeOptions } from "./options"
|
||||
|
||||
export const Serde = Symbol('Serde')
|
||||
|
||||
|
||||
function decorateContainer(options: ContainerOptions, constructor: any) {
|
||||
if (constructor[Serde] == null) {
|
||||
constructor[Serde] = new SerdeOptions(constructor, options)
|
||||
} else {
|
||||
constructor[Serde].options = options
|
||||
}
|
||||
|
||||
return constructor
|
||||
}
|
||||
|
||||
function decorateProperty(options: PropertyOptions, target: any, property: PropertyKey) {
|
||||
let constructor
|
||||
if (typeof target === 'function') {
|
||||
constructor = target.constructor
|
||||
} else {
|
||||
constructor = target
|
||||
}
|
||||
|
||||
if (constructor[Serde] == null) {
|
||||
constructor[Serde] = SerdeOptions.from(target)
|
||||
}
|
||||
|
||||
constructor[Serde].properties.set(property, options)
|
||||
}
|
||||
|
||||
|
||||
export function serde(options: ContainerOptions | PropertyOptions) {
|
||||
return function(target: any, property?: PropertyKey) {
|
||||
if (property != null) {
|
||||
return decorateProperty(options as PropertyOptions, target, property)
|
||||
} else {
|
||||
return decorateContainer(options, target)
|
||||
}
|
||||
}
|
||||
}
|
116
src/json.ts
116
src/json.ts
|
@ -1,9 +1,11 @@
|
|||
import { DefaultIterableAccessImpl, DefaultMapAccessImpl, Deserialize, Deserializer, IterableAccess, MapAccess, Visitor } from './de'
|
||||
import { Serializer, serializeWith } from './ser'
|
||||
import { IterableSerializer, ObjectSerializer, Serializable, Serializer, serializeWith } from './ser'
|
||||
import { mixin, Nullable } from './utils'
|
||||
|
||||
export function toString(value: any): string {
|
||||
return serializeWith(new JSONSerializer(), value)!
|
||||
const serializer = new JSONSerializer()
|
||||
serializeWith(serializer, value)
|
||||
return serializer.output
|
||||
}
|
||||
|
||||
export function fromString<T, D extends Deserialize<T>>(value: string, into: D): T {
|
||||
|
@ -225,9 +227,101 @@ class StringBuffer {
|
|||
}
|
||||
}
|
||||
|
||||
export class JSONSerializer implements Serializer<string> {
|
||||
serializeAny(value?: any): string {
|
||||
return JSON.stringify(value)
|
||||
class JSONObjectSerializer implements ObjectSerializer<void> {
|
||||
private ser: JSONSerializer
|
||||
private first: boolean = true
|
||||
|
||||
constructor(serializer: JSONSerializer) {
|
||||
this.ser = serializer
|
||||
serializer.write('{')
|
||||
}
|
||||
|
||||
serializeKey<U extends Serializable>(key: U): void {
|
||||
if (!this.first) {
|
||||
this.ser.write(',')
|
||||
} else {
|
||||
this.first = false
|
||||
}
|
||||
|
||||
serializeWith(this.ser, key)
|
||||
}
|
||||
|
||||
serializeValue<U extends Serializable>(value: U): void {
|
||||
this.ser.write(':')
|
||||
serializeWith(this.ser, value)
|
||||
}
|
||||
|
||||
end(): void {
|
||||
this.ser.write('}')
|
||||
}
|
||||
}
|
||||
|
||||
class JSONIterableSerializer implements IterableSerializer<void> {
|
||||
private ser: JSONSerializer
|
||||
private first: boolean = true
|
||||
|
||||
constructor(serializer: JSONSerializer) {
|
||||
this.ser = serializer
|
||||
serializer.write('[')
|
||||
}
|
||||
|
||||
serializeElement<U extends Serializable>(element: U): void {
|
||||
if (!this.first) {
|
||||
this.ser.write(',')
|
||||
} else {
|
||||
this.first = false
|
||||
}
|
||||
|
||||
serializeWith(this.ser, element)
|
||||
}
|
||||
|
||||
end(): void {
|
||||
this.ser.write(']')
|
||||
}
|
||||
}
|
||||
|
||||
export class JSONSerializer implements Serializer<void> {
|
||||
output: string = ''
|
||||
|
||||
write(value: string) {
|
||||
this.output += value
|
||||
}
|
||||
|
||||
serializeString(value: string) {
|
||||
this.write(`"${value}"`)
|
||||
}
|
||||
|
||||
serializeBoolean(value: boolean): void {
|
||||
this.write(value.toString())
|
||||
}
|
||||
|
||||
serializeSymbol(value: symbol): void {
|
||||
const key = Symbol.keyFor(value)
|
||||
if (key) {
|
||||
this.write(key)
|
||||
} else {
|
||||
return this.serializeString(value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
serializeObject(): ObjectSerializer<void> {
|
||||
return new JSONObjectSerializer(this)
|
||||
}
|
||||
|
||||
serializeNumber(value: number) {
|
||||
this.write(value.toString())
|
||||
}
|
||||
|
||||
serializeBigInt(value: bigint) {
|
||||
this.write(value.toString())
|
||||
}
|
||||
|
||||
serializeIterable(): IterableSerializer<void> {
|
||||
return new JSONIterableSerializer(this)
|
||||
}
|
||||
|
||||
serializeNull() {
|
||||
return this.write('null')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -355,14 +449,6 @@ export class JSONDeserializer implements Deserializer {
|
|||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
deserializeFunction?<T, V extends Visitor<T>>(_visitor: V): T {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
deserializeMap?<T, V extends Visitor<T>>(_visitor: V): T {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
deserializeIterable?<T, V extends Visitor<T>>(visitor: V): T {
|
||||
let next = this.buffer.take()
|
||||
if (next.next() === Token.LeftSquare) {
|
||||
|
@ -379,10 +465,6 @@ export class JSONDeserializer implements Deserializer {
|
|||
throw unexpected('[', next.toString(), this.buffer.position)
|
||||
}
|
||||
}
|
||||
|
||||
deserializeClass?<T, V extends Visitor<T>>(name: string, fields: string[], visitor: V): T {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
111
src/options.ts
Normal file
111
src/options.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { CaseConvention, convertCase } from './case'
|
||||
import { isNumber, isString, Morphism } from './utils'
|
||||
|
||||
|
||||
export interface RenameOptions {
|
||||
serialize?: string
|
||||
deserialize?: string
|
||||
}
|
||||
|
||||
export interface RenameAllOptions {
|
||||
serialize?: CaseConvention
|
||||
deserialize?: CaseConvention
|
||||
}
|
||||
|
||||
export interface ContainerOptions {
|
||||
// deserialization only
|
||||
default?: <T>() => T
|
||||
rename?: RenameOptions | string
|
||||
renameAll?: RenameAllOptions | CaseConvention
|
||||
// deserialization only
|
||||
denyUnknownFields?: boolean
|
||||
tag?: string
|
||||
content?: string
|
||||
untagged?: boolean
|
||||
expecting?: string
|
||||
}
|
||||
|
||||
export interface ConditionalSkipOptions {
|
||||
if: (value: any) => boolean
|
||||
}
|
||||
|
||||
export interface SkipOptions {
|
||||
serializing?: ConditionalSkipOptions | boolean
|
||||
deserializing?: ConditionalSkipOptions | boolean
|
||||
}
|
||||
|
||||
export interface PropertyOptions {
|
||||
alias?: string
|
||||
flatten?: boolean
|
||||
rename?: RenameOptions | string
|
||||
serializeWith?: Morphism
|
||||
deserializeWith: Morphism
|
||||
skip?: SkipOptions | boolean
|
||||
}
|
||||
|
||||
export const Stage = Object.freeze({
|
||||
Serialize: 0,
|
||||
Deserialize: 1
|
||||
} as const)
|
||||
|
||||
export type Stage = typeof Stage[keyof typeof Stage]
|
||||
|
||||
export class SerdeOptions {
|
||||
private readonly target: any
|
||||
options: ContainerOptions
|
||||
properties: Map<string, PropertyOptions>
|
||||
|
||||
constructor(target: any, options: ContainerOptions = {}, properties: Map<string, PropertyOptions> = new Map()) {
|
||||
this.target = target
|
||||
this.options = options
|
||||
this.properties = properties
|
||||
}
|
||||
|
||||
static from(target: any) {
|
||||
return new this(target)
|
||||
}
|
||||
|
||||
getClassName(stage: Stage) {
|
||||
if (isString(this.options.rename)) {
|
||||
return this.options.rename
|
||||
} else if (stage === Stage.Serialize && isString(this.options.rename?.serialize)) {
|
||||
return this.options.rename.serialize
|
||||
} else if (stage === Stage.Deserialize && isString(this.options.rename?.deserialize)) {
|
||||
return this.options.rename.deserialize
|
||||
} else {
|
||||
return this.target.constructor.name
|
||||
}
|
||||
}
|
||||
|
||||
private getPropertyRename(property: string, stage: Stage, options: PropertyOptions) {
|
||||
if (options != null) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return property
|
||||
}
|
||||
|
||||
private getPropertyCase(name: string, stage: Stage) {
|
||||
if (isNumber(this.options.renameAll)) {
|
||||
return convertCase(name, this.options.renameAll)
|
||||
} else if (stage === Stage.Serialize && isNumber(this.options.renameAll?.serialize)) {
|
||||
return convertCase(name, this.options.renameAll.serialize)
|
||||
} else if (stage === Stage.Deserialize && isNumber(this.options.renameAll?.deserialize)) {
|
||||
return convertCase(name, this.options.renameAll.deserialize)
|
||||
} else {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
getPropertyName(property: string, stage: Stage) {
|
||||
const options = this.properties.get(property)
|
||||
const name = options != null ? this.getPropertyRename(property, stage, options) : property
|
||||
return this.getPropertyCase(name, stage)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import { ifNull, isFunction, isIterable, isPlainObject, Nullable, orElse } from '../utils'
|
||||
import { ClassSerializer, IterableSerializer, ObjectSerializer, Serializable, Serializer } from './interface'
|
||||
import { Serde } from '../decorator'
|
||||
import { SerdeOptions } from '../options'
|
||||
import { ifNull, isFunction, isIterable, Nullable, orElse } from '../utils'
|
||||
import { IterableSerializer, ObjectSerializer, Serializable, Serializer } from './interface'
|
||||
|
||||
const unhandledType = (serializer: any, value: any) => new TypeError(`'${serializer.constructor.name}' has no method for value type '${typeof value}'`)
|
||||
|
||||
|
@ -14,22 +16,10 @@ function serializeEntries<T, K extends Serializable, V extends Serializable, E e
|
|||
return serializer.end()
|
||||
}
|
||||
|
||||
function serializeObject<T, K extends PropertyKey, V extends Serializable, R extends Record<K, V>>(serializer: ObjectSerializer<T>, value: R) {
|
||||
function serializeObject<T, K extends PropertyKey, V extends Serializable, R extends Record<K, V>>(serializer: ObjectSerializer<T>, value: R, options?: SerdeOptions) {
|
||||
return serializeEntries(serializer, Object.entries(value) as Iterable<[K, V]>)
|
||||
}
|
||||
|
||||
function getClassName(value: any): Nullable<string> {
|
||||
return value?.constructor.name
|
||||
}
|
||||
|
||||
function serializeClass<T, K extends PropertyKey, V extends Serializable, R extends Record<K, V>>(serializer: ClassSerializer<T>, value: R) {
|
||||
for (const prop in value) {
|
||||
serializer.serializeField(prop, value[prop])
|
||||
}
|
||||
|
||||
return serializer.end()
|
||||
}
|
||||
|
||||
function serializeIter<T, V extends Iterable<any>>(serializer: IterableSerializer<T>, value: V) {
|
||||
let state
|
||||
|
||||
|
@ -40,8 +30,12 @@ function serializeIter<T, V extends Iterable<any>>(serializer: IterableSerialize
|
|||
return serializer.end()
|
||||
}
|
||||
|
||||
// dispatches in the order of serialize<type> -> serializeAny -> throw TypeError
|
||||
export function serializeWith<T>(serializer: Serializer<T>, value: Serializable): Nullable<T> {
|
||||
function defaultOptions(value: any) {
|
||||
return value.constructor[Serde]
|
||||
}
|
||||
|
||||
// dispatches in the order of serializeType -> serializeAny -> throw TypeError
|
||||
export function serializeWith<T>(serializer: Serializer<T>, value: Serializable, optionsGetter: (value: any) => Nullable<SerdeOptions> = defaultOptions): Nullable<T> {
|
||||
// prepare fallback methods
|
||||
const serializeAny = orElse(
|
||||
serializer,
|
||||
|
@ -58,18 +52,12 @@ export function serializeWith<T>(serializer: Serializer<T>, value: Serializable)
|
|||
case 'boolean': return serialize(serializer.serializeBoolean)
|
||||
case 'symbol': return serialize(serializer.serializeSymbol)
|
||||
case 'undefined': return serialize(serializer.serializeNull)
|
||||
case 'function': return serialize(serializer.serializeFunction)
|
||||
|
||||
case 'object':
|
||||
if (value instanceof Map && isFunction(serializer.serializeMap)) {
|
||||
return serializeEntries(serializer.serializeMap(), value)
|
||||
} else if (isIterable(value) && isFunction(serializer.serializeIterable)) {
|
||||
if (isIterable(value) && isFunction(serializer.serializeIterable)) {
|
||||
return serializeIter(serializer.serializeIterable(), value)
|
||||
} else if (isFunction(serializer.serializeClass) && !isPlainObject(value)) {
|
||||
const name = getClassName(value)
|
||||
return serializeClass(serializer.serializeClass(name!), value as any)
|
||||
} else if (isFunction(serializer.serializeObject)) {
|
||||
return serializeObject(serializer.serializeObject!(), value as Record<PropertyKey, any>)
|
||||
return serializeObject(serializer.serializeObject!(), value as Record<PropertyKey, any>, optionsGetter(value))
|
||||
} // deliberate fallthrough when the above fail
|
||||
|
||||
default: return serializeAny(value)
|
||||
|
|
|
@ -11,10 +11,10 @@ export interface IterableSerializer<T = void> {
|
|||
end(): T
|
||||
}
|
||||
|
||||
export interface ClassSerializer<T = void> {
|
||||
serializeField<U extends Serializable>(name: PropertyKey, value: U): T
|
||||
end(): T
|
||||
}
|
||||
//export interface ClassSerializer<T = void> {
|
||||
// serializeField<U extends Serializable>(name: PropertyKey, value: U): T
|
||||
// end(): T
|
||||
//}
|
||||
|
||||
const TypeSerializerMethods = [
|
||||
'serializeString',
|
||||
|
@ -22,12 +22,11 @@ const TypeSerializerMethods = [
|
|||
'serializeBigInt',
|
||||
'serializeBoolean',
|
||||
'serializeSymbol',
|
||||
'serializeMap',
|
||||
//'serializeMap',
|
||||
'serializeIterable',
|
||||
'serializeNull',
|
||||
'serializeObject',
|
||||
'serializeInstance',
|
||||
'serializeFunction'
|
||||
//'serializeInstance',
|
||||
] as const
|
||||
|
||||
interface TypeSerializer<T> {
|
||||
|
@ -38,10 +37,9 @@ interface TypeSerializer<T> {
|
|||
serializeSymbol(value: Symbol): T
|
||||
serializeNull(): T
|
||||
serializeObject(): ObjectSerializer<T>
|
||||
serializeFunction?(value: Function): T
|
||||
serializeMap?(): ObjectSerializer<T>
|
||||
// serializeMap?(): ObjectSerializer<T>
|
||||
serializeIterable?(): IterableSerializer<T>
|
||||
serializeClass?(name: PropertyKey): ClassSerializer<T>
|
||||
//serializeClass?(name: PropertyKey): ClassSerializer<T>
|
||||
}
|
||||
|
||||
const AnySerializerMethods = ['serializeAny']
|
||||
|
|
|
@ -1,36 +1,16 @@
|
|||
import { CaseConvention, Constructor } from '../utils'
|
||||
import { Constructor } from '../utils'
|
||||
import { serializeWith } from './impl'
|
||||
import { isGenericSerializer, Serializer } from './interface'
|
||||
|
||||
export interface SerializationOptions {
|
||||
default?: <T>() => T
|
||||
rename?: string
|
||||
renameAll?: CaseConvention
|
||||
tag?: string
|
||||
untagged?: boolean
|
||||
withInherited?: boolean
|
||||
}
|
||||
|
||||
const DefaultSerializationOptions: SerializationOptions = {
|
||||
withInherited: true
|
||||
}
|
||||
|
||||
export function serialize(options?: SerializationOptions) {
|
||||
options = {
|
||||
...DefaultSerializationOptions,
|
||||
...options
|
||||
}
|
||||
|
||||
return function <T extends Constructor>(constructor: T) {
|
||||
return class Serializable extends constructor implements Serializable {
|
||||
static name = constructor.name
|
||||
serialize<U>(serializer: Serializer<U>): U {
|
||||
// shortcut for serializers with only the serializeAny method
|
||||
if (isGenericSerializer(serializer)) {
|
||||
return serializer.serializeAny!(this) as U
|
||||
} else {
|
||||
return serializeWith(serializer, this) as U
|
||||
}
|
||||
export function serialize<T extends Constructor>(constructor: T) {
|
||||
return class Serializable extends constructor implements Serializable {
|
||||
static name = constructor.name
|
||||
serialize<U>(serializer: Serializer<U>): U {
|
||||
// shortcut for serializers with only the serializeAny method
|
||||
if (isGenericSerializer(serializer)) {
|
||||
return serializer.serializeAny!(this) as U
|
||||
} else {
|
||||
return serializeWith(serializer, this) as U
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
37
src/test.ts
Normal file
37
src/test.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { CaseConvention } from './case'
|
||||
import { deserialize } from './de'
|
||||
import { Serde, serde } from './decorator'
|
||||
import { fromString, toString } from './json'
|
||||
import { serialize } from './ser'
|
||||
|
||||
const InnerStruct = deserialize(serialize(
|
||||
class {
|
||||
c = 'awawa'
|
||||
}))
|
||||
|
||||
const TestStruct = deserialize(serialize(
|
||||
serde({ renameAll: CaseConvention.PascalCase })(
|
||||
class {
|
||||
a = 1
|
||||
b
|
||||
inner = new InnerStruct()
|
||||
d = true
|
||||
e = Math.pow(2, 53)
|
||||
f = Symbol('test')
|
||||
g = [1, 'a', [3]]
|
||||
|
||||
constructor() {
|
||||
this.b = new Map()
|
||||
this.b.set('test key', 2)
|
||||
}
|
||||
})))
|
||||
|
||||
const test = new TestStruct()
|
||||
console.log(test)
|
||||
const value = toString(test)
|
||||
console.log('A', value)
|
||||
const test2 = fromString(value, TestStruct)
|
||||
console.log(test2)
|
||||
|
||||
|
||||
|
52
src/utils.ts
52
src/utils.ts
|
@ -1,5 +1,9 @@
|
|||
export type Nullable<T> = T | undefined
|
||||
|
||||
export interface Morphism<T = any, U = any> {
|
||||
(value: T): U
|
||||
}
|
||||
|
||||
export type Primitive = string | number | boolean | symbol | bigint | null | undefined
|
||||
|
||||
export interface ToString {
|
||||
|
@ -22,21 +26,16 @@ export function isIterable(value: any): value is Iterable<any> {
|
|||
return isFunction(value[Symbol.iterator])
|
||||
}
|
||||
|
||||
export function isString(value: any): value is string {
|
||||
return typeof value === 'string'
|
||||
}
|
||||
|
||||
export function isNumber(value: any): value is number {
|
||||
return !isNaN(value)
|
||||
}
|
||||
|
||||
export type Constructor<T = any> = new (...args: any[]) => T
|
||||
|
||||
export const CaseConvention = Object.freeze({
|
||||
Lowercase: 0,
|
||||
Uppercase: 1,
|
||||
PascalCase: 2,
|
||||
CamelCase: 3,
|
||||
SnakeCase: 4,
|
||||
ScreamingSnakeCase: 5,
|
||||
KebabCase: 6,
|
||||
ScreamingKebabCase: 7
|
||||
} as const)
|
||||
|
||||
export type CaseConvention = typeof CaseConvention[keyof typeof CaseConvention]
|
||||
|
||||
export function orElse(thisArg: any, a: Nullable<Function>, b: Function) {
|
||||
return function(...args: any) {
|
||||
const fn = a != null ? a : b
|
||||
|
@ -46,7 +45,7 @@ export function orElse(thisArg: any, a: Nullable<Function>, b: Function) {
|
|||
|
||||
export function ifNull(thisArg: any, b: Function, ...args: any) {
|
||||
return function(a: Nullable<Function>) {
|
||||
return orElse(thisArg, a, b).call(thisArg, args)
|
||||
return orElse(thisArg, a, b).apply(thisArg, args)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,3 +69,28 @@ export function mixin<U = any>(impl: Function) {
|
|||
}
|
||||
}
|
||||
|
||||
type AnyFunc = (...arg: any) => any
|
||||
|
||||
type PipeArgs<F extends AnyFunc[], Acc extends AnyFunc[] = []> = F extends [
|
||||
(...args: infer A) => infer B
|
||||
]
|
||||
? [...Acc, (...args: A) => B]
|
||||
: F extends [(...args: infer A) => any, ...infer Tail]
|
||||
? Tail extends [(arg: infer B) => any, ...any[]]
|
||||
? PipeArgs<Tail, [...Acc, (...args: A) => B]>
|
||||
: Acc
|
||||
: Acc
|
||||
|
||||
type LastFnReturnType<F extends Array<AnyFunc>, Else = never> = F extends [
|
||||
...any[],
|
||||
(...arg: any) => infer R
|
||||
] ? R : Else
|
||||
|
||||
export function pipe<FirstFn extends AnyFunc, F extends AnyFunc[]>(
|
||||
arg: Parameters<FirstFn>[0],
|
||||
firstFn: FirstFn,
|
||||
...fns: PipeArgs<F> extends F ? F : PipeArgs<F>
|
||||
): LastFnReturnType<F, ReturnType<FirstFn>> {
|
||||
return (fns as AnyFunc[]).reduce((acc, fn) => fn(acc), firstFn(arg))
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue