refactor structure; custom json serializer

This commit is contained in:
Rowan 2025-05-16 22:20:10 -05:00
parent 7116abb2ee
commit b2f19f1e0c
12 changed files with 428 additions and 144 deletions

63
src/case.ts Normal file
View 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)
}
}

View file

@ -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.')
}
}

View file

@ -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> {

View file

@ -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
View 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)
}
}
}

View file

@ -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
View 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)
}
}

View file

@ -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)

View file

@ -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']

View file

@ -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
View 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)

View file

@ -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))
}