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
|
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 {
|
visitIterable?(access: IterableAccess): T {
|
||||||
const result = new Array(access.sizeHint())
|
const result = new Array(access.sizeHint())
|
||||||
let element
|
let element
|
||||||
|
@ -73,9 +58,5 @@ export class GenericVisitor<T> implements Visitor<T> {
|
||||||
|
|
||||||
return result as 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
|
visitSymbol(value: symbol): T
|
||||||
visitNull(): T
|
visitNull(): T
|
||||||
visitObject(value: MapAccess): T
|
visitObject(value: MapAccess): T
|
||||||
visitFunction?(value: Function): T
|
|
||||||
visitMap?(value: MapAccess): T
|
|
||||||
visitIterable?(value: IterableAccess): T
|
visitIterable?(value: IterableAccess): T
|
||||||
visitClass?(name: string, fields: string[], value: any): T
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Deserializer {
|
export interface Deserializer {
|
||||||
|
@ -77,10 +74,7 @@ export interface Deserializer {
|
||||||
deserializeSymbol<T, V extends Visitor<T>>(visitor: V): T
|
deserializeSymbol<T, V extends Visitor<T>>(visitor: V): T
|
||||||
deserializeNull<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
|
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
|
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> {
|
export interface Deserialize<T> {
|
||||||
|
|
|
@ -1,30 +1,16 @@
|
||||||
import { CaseConvention, Constructor, staticImplements } from '../utils'
|
import { Constructor, staticImplements } from '../utils'
|
||||||
import { GenericVisitor } from './generic'
|
import { GenericVisitor } from './generic'
|
||||||
import { Deserialize, Deserializer } from './interface'
|
import { Deserialize, Deserializer } from './interface'
|
||||||
|
|
||||||
export interface DeserializationOptions {
|
export function deserialize<T, C extends Constructor>(constructor: C) {
|
||||||
rename?: string
|
@staticImplements<Deserialize<T>>()
|
||||||
renameAll?: CaseConvention
|
class Deserializable extends constructor {
|
||||||
}
|
static deserialize<D extends Deserializer>(deserializer: D): T {
|
||||||
|
const visitor = new GenericVisitor<T>()
|
||||||
const DefaultDeserializationOptions = {}
|
return deserializer.deserializeAny(visitor)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 { 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'
|
import { mixin, Nullable } from './utils'
|
||||||
|
|
||||||
export function toString(value: any): string {
|
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 {
|
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> {
|
class JSONObjectSerializer implements ObjectSerializer<void> {
|
||||||
serializeAny(value?: any): string {
|
private ser: JSONSerializer
|
||||||
return JSON.stringify(value)
|
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.')
|
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 {
|
deserializeIterable?<T, V extends Visitor<T>>(visitor: V): T {
|
||||||
let next = this.buffer.take()
|
let next = this.buffer.take()
|
||||||
if (next.next() === Token.LeftSquare) {
|
if (next.next() === Token.LeftSquare) {
|
||||||
|
@ -379,10 +465,6 @@ export class JSONDeserializer implements Deserializer {
|
||||||
throw unexpected('[', next.toString(), this.buffer.position)
|
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 { Serde } from '../decorator'
|
||||||
import { ClassSerializer, IterableSerializer, ObjectSerializer, Serializable, Serializer } from './interface'
|
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}'`)
|
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()
|
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]>)
|
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) {
|
function serializeIter<T, V extends Iterable<any>>(serializer: IterableSerializer<T>, value: V) {
|
||||||
let state
|
let state
|
||||||
|
|
||||||
|
@ -40,8 +30,12 @@ function serializeIter<T, V extends Iterable<any>>(serializer: IterableSerialize
|
||||||
return serializer.end()
|
return serializer.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
// dispatches in the order of serialize<type> -> serializeAny -> throw TypeError
|
function defaultOptions(value: any) {
|
||||||
export function serializeWith<T>(serializer: Serializer<T>, value: Serializable): Nullable<T> {
|
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
|
// prepare fallback methods
|
||||||
const serializeAny = orElse(
|
const serializeAny = orElse(
|
||||||
serializer,
|
serializer,
|
||||||
|
@ -58,18 +52,12 @@ export function serializeWith<T>(serializer: Serializer<T>, value: Serializable)
|
||||||
case 'boolean': return serialize(serializer.serializeBoolean)
|
case 'boolean': return serialize(serializer.serializeBoolean)
|
||||||
case 'symbol': return serialize(serializer.serializeSymbol)
|
case 'symbol': return serialize(serializer.serializeSymbol)
|
||||||
case 'undefined': return serialize(serializer.serializeNull)
|
case 'undefined': return serialize(serializer.serializeNull)
|
||||||
case 'function': return serialize(serializer.serializeFunction)
|
|
||||||
|
|
||||||
case 'object':
|
case 'object':
|
||||||
if (value instanceof Map && isFunction(serializer.serializeMap)) {
|
if (isIterable(value) && isFunction(serializer.serializeIterable)) {
|
||||||
return serializeEntries(serializer.serializeMap(), value)
|
|
||||||
} else if (isIterable(value) && isFunction(serializer.serializeIterable)) {
|
|
||||||
return serializeIter(serializer.serializeIterable(), value)
|
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)) {
|
} 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
|
} // deliberate fallthrough when the above fail
|
||||||
|
|
||||||
default: return serializeAny(value)
|
default: return serializeAny(value)
|
||||||
|
|
|
@ -11,10 +11,10 @@ export interface IterableSerializer<T = void> {
|
||||||
end(): T
|
end(): T
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClassSerializer<T = void> {
|
//export interface ClassSerializer<T = void> {
|
||||||
serializeField<U extends Serializable>(name: PropertyKey, value: U): T
|
// serializeField<U extends Serializable>(name: PropertyKey, value: U): T
|
||||||
end(): T
|
// end(): T
|
||||||
}
|
//}
|
||||||
|
|
||||||
const TypeSerializerMethods = [
|
const TypeSerializerMethods = [
|
||||||
'serializeString',
|
'serializeString',
|
||||||
|
@ -22,12 +22,11 @@ const TypeSerializerMethods = [
|
||||||
'serializeBigInt',
|
'serializeBigInt',
|
||||||
'serializeBoolean',
|
'serializeBoolean',
|
||||||
'serializeSymbol',
|
'serializeSymbol',
|
||||||
'serializeMap',
|
//'serializeMap',
|
||||||
'serializeIterable',
|
'serializeIterable',
|
||||||
'serializeNull',
|
'serializeNull',
|
||||||
'serializeObject',
|
'serializeObject',
|
||||||
'serializeInstance',
|
//'serializeInstance',
|
||||||
'serializeFunction'
|
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
interface TypeSerializer<T> {
|
interface TypeSerializer<T> {
|
||||||
|
@ -38,10 +37,9 @@ interface TypeSerializer<T> {
|
||||||
serializeSymbol(value: Symbol): T
|
serializeSymbol(value: Symbol): T
|
||||||
serializeNull(): T
|
serializeNull(): T
|
||||||
serializeObject(): ObjectSerializer<T>
|
serializeObject(): ObjectSerializer<T>
|
||||||
serializeFunction?(value: Function): T
|
// serializeMap?(): ObjectSerializer<T>
|
||||||
serializeMap?(): ObjectSerializer<T>
|
|
||||||
serializeIterable?(): IterableSerializer<T>
|
serializeIterable?(): IterableSerializer<T>
|
||||||
serializeClass?(name: PropertyKey): ClassSerializer<T>
|
//serializeClass?(name: PropertyKey): ClassSerializer<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnySerializerMethods = ['serializeAny']
|
const AnySerializerMethods = ['serializeAny']
|
||||||
|
|
|
@ -1,36 +1,16 @@
|
||||||
import { CaseConvention, Constructor } from '../utils'
|
import { Constructor } from '../utils'
|
||||||
import { serializeWith } from './impl'
|
import { serializeWith } from './impl'
|
||||||
import { isGenericSerializer, Serializer } from './interface'
|
import { isGenericSerializer, Serializer } from './interface'
|
||||||
|
|
||||||
export interface SerializationOptions {
|
export function serialize<T extends Constructor>(constructor: T) {
|
||||||
default?: <T>() => T
|
return class Serializable extends constructor implements Serializable {
|
||||||
rename?: string
|
static name = constructor.name
|
||||||
renameAll?: CaseConvention
|
serialize<U>(serializer: Serializer<U>): U {
|
||||||
tag?: string
|
// shortcut for serializers with only the serializeAny method
|
||||||
untagged?: boolean
|
if (isGenericSerializer(serializer)) {
|
||||||
withInherited?: boolean
|
return serializer.serializeAny!(this) as U
|
||||||
}
|
} else {
|
||||||
|
return serializeWith(serializer, this) as U
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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 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 type Primitive = string | number | boolean | symbol | bigint | null | undefined
|
||||||
|
|
||||||
export interface ToString {
|
export interface ToString {
|
||||||
|
@ -22,21 +26,16 @@ export function isIterable(value: any): value is Iterable<any> {
|
||||||
return isFunction(value[Symbol.iterator])
|
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 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) {
|
export function orElse(thisArg: any, a: Nullable<Function>, b: Function) {
|
||||||
return function(...args: any) {
|
return function(...args: any) {
|
||||||
const fn = a != null ? a : b
|
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) {
|
export function ifNull(thisArg: any, b: Function, ...args: any) {
|
||||||
return function(a: Nullable<Function>) {
|
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