wip class serde
This commit is contained in:
parent
aae64081ad
commit
e43331210f
9 changed files with 96 additions and 532 deletions
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -9,7 +9,6 @@
|
|||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/text-encoding": "^0.0.40",
|
||||
"esbuild": "^0.25.4",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
@ -439,13 +438,6 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/text-encoding": {
|
||||
"version": "0.0.40",
|
||||
"resolved": "https://registry.npmjs.org/@types/text-encoding/-/text-encoding-0.0.40.tgz",
|
||||
"integrity": "sha512-dHzoIdwBfY7jcSTTt6XBkaeiuFQAQD7r/7aJySKDdHkYBCDOvs9jPVt4NYXuwBMn89PP6gSd29WubIS19wTiXg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||
|
|
26
src/de/impl.ts
Normal file
26
src/de/impl.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { SerdeOptions, Stage } from '../options'
|
||||
import { GenericVisitor } from './generic'
|
||||
import { Deserialize, Deserializer } from './interface'
|
||||
|
||||
type DeserializeConstructor<T> = Deserialize<T> & { new(): Deserialize<T> }
|
||||
|
||||
export function deserializeWith<T, D extends Deserializer, E extends DeserializeConstructor<T>>(deserializer: D, into: E, options: SerdeOptions): T {
|
||||
const visitor = new GenericVisitor<T>()
|
||||
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)
|
||||
}
|
||||
|
|
@ -1,31 +1,8 @@
|
|||
import { getMetadata, Serde } from '../decorator'
|
||||
import { SerdeOptions, Stage } from '../options'
|
||||
import { Constructor, staticImplements } from '../utils'
|
||||
import { GenericVisitor } from './generic'
|
||||
import { deserializeWith } from './impl'
|
||||
import { Deserialize, Deserializer } from './interface'
|
||||
|
||||
type DeserializeConstructor<T> = Deserialize<T> & { new(): Deserialize<T> }
|
||||
|
||||
function deserializeWith<T, D extends Deserializer, E extends DeserializeConstructor<T>>(deserializer: D, into: E, options: SerdeOptions): T {
|
||||
const visitor = new GenericVisitor<T>()
|
||||
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<T, C extends Constructor>(constructor: C) {
|
||||
@staticImplements<Deserialize<T>>()
|
||||
class Deserializable extends constructor {
|
||||
|
|
|
@ -46,6 +46,10 @@ export function getMetadata(value: any) {
|
|||
|
||||
export function register(registry: Registry = GlobalRegistry) {
|
||||
return function(target: any) {
|
||||
if (target[Serde] == null) {
|
||||
target[Serde] = SerdeOptions.from(target)
|
||||
}
|
||||
|
||||
registry.add(target)
|
||||
return target
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export * as ser from './ser'
|
||||
export * as de from './de'
|
||||
export * as json from './json'
|
||||
export * from './decorator'
|
||||
export * from './options'
|
||||
|
||||
|
|
481
src/json.ts
481
src/json.ts
|
@ -1,481 +0,0 @@
|
|||
import { DefaultIterableAccessImpl, DefaultMapAccessImpl, Deserialize, Deserializer, IterableAccess, MapAccess, Visitor } from './de'
|
||||
import { GlobalRegistry, Registry } from './registry'
|
||||
import { IterableSerializer, ObjectSerializer, Serializable, Serializer, serializeWith } from './ser'
|
||||
import { mixin, Nullable } from './utils'
|
||||
|
||||
export function toString(value: any): string {
|
||||
const serializer = new JSONSerializer()
|
||||
serializeWith(serializer, value)
|
||||
return serializer.output
|
||||
}
|
||||
|
||||
export function fromString<T, D extends Deserialize<T>>(value: string, into: D): T {
|
||||
const deserializer = JSONDeserializer.fromString(value)
|
||||
return into.deserialize(deserializer)
|
||||
}
|
||||
|
||||
type Byte = number
|
||||
|
||||
const clamp = (value: number, min: number, max: number): number => {
|
||||
return Math.min(Math.max(value, min), max)
|
||||
}
|
||||
|
||||
const isNumeric = (value: any): value is number => {
|
||||
return !isNaN(value)
|
||||
}
|
||||
|
||||
const isNumericToken = (value: Byte) => {
|
||||
return value === Token.Period || value === Token.Hyphen || Token.Digit.includes(value)
|
||||
}
|
||||
|
||||
interface Predicate<T> {
|
||||
(value: T): boolean
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
const b = (strings: TemplateStringsArray) => encoder.encode(strings[0])
|
||||
const char = (strings: TemplateStringsArray) => b(strings)[0]
|
||||
|
||||
const Literal = Object.freeze({
|
||||
True: b`true`,
|
||||
False: b`false`
|
||||
} as const)
|
||||
|
||||
const Token = Object.freeze({
|
||||
Space: char` `,
|
||||
LeftCurly: char`{`,
|
||||
RightCurly: char`}`,
|
||||
LeftSquare: char`[`,
|
||||
RightSquare: char`]`,
|
||||
Quote: char`"`,
|
||||
ForwardSlash: char`\\`,
|
||||
Digit: b`0123456789`,
|
||||
Hyphen: char`-`,
|
||||
Period: char`.`,
|
||||
Comma: char`,`,
|
||||
Colon: char`:`
|
||||
} as const)
|
||||
|
||||
|
||||
export interface CommaSeparated extends MapAccess, IterableAccess { }
|
||||
@mixin<MapAccess>(DefaultMapAccessImpl)
|
||||
@mixin<IterableAccess>(DefaultIterableAccessImpl)
|
||||
export class CommaSeparated implements MapAccess, IterableAccess {
|
||||
private readonly de: JSONDeserializer
|
||||
private first: boolean = true
|
||||
|
||||
constructor(deserializer: JSONDeserializer) {
|
||||
this.de = deserializer
|
||||
}
|
||||
|
||||
nextKeySeed<T, K extends Deserialize<T>>(seed: K): Nullable<T> {
|
||||
if (this.de.buffer.peek().next() === Token.RightCurly) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.first) {
|
||||
const take = this.de.buffer.take()
|
||||
if (take.next() !== Token.Comma) {
|
||||
throw unexpected(',', take.toString(), this.de.buffer.position)
|
||||
}
|
||||
}
|
||||
|
||||
this.first = false
|
||||
return seed.deserialize(this.de)
|
||||
}
|
||||
|
||||
nextValueSeed<T, V extends Deserialize<T>>(seed: V): Nullable<T> {
|
||||
const next = this.de.buffer.next()
|
||||
if (next !== Token.Colon) {
|
||||
throw unexpected(':', next.toString(), this.de.buffer.position)
|
||||
}
|
||||
|
||||
return seed.deserialize(this.de)
|
||||
}
|
||||
|
||||
nextElementSeed<T, I extends Deserialize<T>>(seed: I): Nullable<T> {
|
||||
if (this.de.buffer.peek().next() === Token.RightSquare) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.first) {
|
||||
const take = this.de.buffer.take()
|
||||
if (take.next() !== Token.Comma) {
|
||||
throw unexpected(',', take.toString(), this.de.buffer.position)
|
||||
}
|
||||
}
|
||||
|
||||
this.first = false
|
||||
return seed.deserialize(this.de)
|
||||
}
|
||||
}
|
||||
|
||||
class StringBuffer {
|
||||
private readonly view: Uint8Array
|
||||
private index: number = 0
|
||||
private readonly encoder: TextEncoder
|
||||
private readonly decoder: TextDecoder
|
||||
|
||||
get position() {
|
||||
return this.index
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.view.byteLength
|
||||
}
|
||||
|
||||
constructor(view: Uint8Array, encoder: TextEncoder = new TextEncoder(), decoder: TextDecoder = new TextDecoder()) {
|
||||
this.view = view
|
||||
this.encoder = encoder
|
||||
this.decoder = decoder
|
||||
}
|
||||
|
||||
static fromArrayBuffer(value: ArrayBuffer, encoder?: TextEncoder, decoder?: TextDecoder): StringBuffer {
|
||||
return new this(new Uint8Array(value), encoder, decoder)
|
||||
}
|
||||
|
||||
static fromString(value: string, encoder: TextEncoder = new TextEncoder(), decoder?: TextDecoder): StringBuffer {
|
||||
return this.fromArrayBuffer(
|
||||
encoder.encode(value),
|
||||
encoder,
|
||||
decoder
|
||||
)
|
||||
}
|
||||
|
||||
next() {
|
||||
const value = this.view[this.index]
|
||||
this.index += 1
|
||||
return value
|
||||
}
|
||||
|
||||
nextChar() {
|
||||
return this.take().toString()
|
||||
}
|
||||
|
||||
done(): boolean {
|
||||
return this.index >= this.view.byteLength
|
||||
}
|
||||
|
||||
toBytes() {
|
||||
return this.view.slice(this.index)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.decoder.decode(this.toBytes())
|
||||
}
|
||||
|
||||
take(limit: number = 1): StringBuffer {
|
||||
const bytes = this.peek(limit)
|
||||
this.index += limit
|
||||
return bytes
|
||||
}
|
||||
|
||||
at(index: number) {
|
||||
return this.view[this.index + index]
|
||||
}
|
||||
|
||||
takeWhile(fn: Predicate<number>): StringBuffer {
|
||||
let index = 0
|
||||
|
||||
while (!this.done() && fn(this.at(index))) {
|
||||
index += 1
|
||||
}
|
||||
|
||||
return this.take(index)
|
||||
}
|
||||
|
||||
drop(limit: number) {
|
||||
this.index += limit
|
||||
return this
|
||||
}
|
||||
|
||||
peek(limit: number = 1): StringBuffer {
|
||||
const index = this.index
|
||||
return this.slice(index, index + limit)
|
||||
}
|
||||
|
||||
startsWith(value: string | ArrayBufferLike): boolean {
|
||||
if (typeof value === 'string') {
|
||||
return this.startsWith(this.encoder.encode(value))
|
||||
}
|
||||
|
||||
const length = value.byteLength
|
||||
const bytes = new Uint8Array(value)
|
||||
|
||||
return this.peek(length).toBytes().every((v, i) => v === bytes[i])
|
||||
}
|
||||
|
||||
slice(start?: number, end?: number) {
|
||||
return new StringBuffer(
|
||||
this.view.subarray(start, end),
|
||||
this.encoder,
|
||||
this.decoder
|
||||
)
|
||||
}
|
||||
|
||||
indexOf(value: number | ArrayBufferLike, start: number = 0) {
|
||||
const search = new Uint8Array(isNumeric(value) ? [value] : value)
|
||||
start = clamp(start, this.index, this.length)
|
||||
const bytes = this.slice(start)
|
||||
|
||||
for (let i = 0, len = bytes.length; i < len; i++) {
|
||||
if (bytes.at(i) === search[0] && bytes.slice(i).startsWith(search)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
this.ser.write(':')
|
||||
}
|
||||
|
||||
serializeValue<U extends Serializable>(value: U): void {
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
const unexpected = (expected: string, actual: string, position: number) => new SyntaxError(`Expected ${expected} at position ${position} (got '${actual}')`)
|
||||
|
||||
export class JSONDeserializer implements Deserializer {
|
||||
private readonly registry: Registry
|
||||
readonly buffer: StringBuffer
|
||||
|
||||
constructor(buffer: StringBuffer, registry: Registry = GlobalRegistry) {
|
||||
this.buffer = buffer
|
||||
this.registry = registry
|
||||
}
|
||||
|
||||
static fromString(value: string): JSONDeserializer {
|
||||
return new this(StringBuffer.fromString(value))
|
||||
}
|
||||
|
||||
deserializeAny<T, V extends Visitor<T>>(visitor: V): T {
|
||||
const peek = this.buffer.peek()
|
||||
const nextByte = peek.take()
|
||||
const byte = nextByte.next()
|
||||
|
||||
switch (true) {
|
||||
case b`n`.includes(byte):
|
||||
return this.deserializeNull(visitor)
|
||||
case b`tf`.includes(byte):
|
||||
return this.deserializeBoolean(visitor)
|
||||
case b`-0123456789`.includes(byte):
|
||||
return this.deserializeNumber(visitor)
|
||||
case Token.Quote === byte:
|
||||
return this.deserializeString(visitor)
|
||||
case Token.LeftSquare === byte:
|
||||
return this.deserializeIterable(visitor)
|
||||
case Token.LeftCurly === byte:
|
||||
return this.deserializeObject(visitor)
|
||||
default:
|
||||
throw new SyntaxError(`Invalid syntax at position ${this.buffer.position}: "${nextByte.toString()}"`)
|
||||
}
|
||||
}
|
||||
|
||||
deserializeNull<T, V extends Visitor<T>>(visitor: V): T {
|
||||
if (this.buffer.startsWith('null')) {
|
||||
this.buffer.take(4)
|
||||
}
|
||||
|
||||
return visitor.visitNull()
|
||||
}
|
||||
|
||||
deserializeObject<T, V extends Visitor<T>>(visitor: V): T {
|
||||
let next = this.buffer.take()
|
||||
if (next.next() === Token.LeftCurly) {
|
||||
|
||||
const value = visitor.visitObject(new CommaSeparated(this))
|
||||
|
||||
next = this.buffer.take()
|
||||
if (next.next() === Token.RightCurly) {
|
||||
return value
|
||||
} else {
|
||||
throw unexpected('}', next.toString(), this.buffer.position)
|
||||
}
|
||||
} else {
|
||||
throw unexpected('{', next.toString(), this.buffer.position)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
deserializeClass<T, V extends Visitor<T>>(name: string, fields: string[], visitor: V): T {
|
||||
const cls = this.registry.get(name)
|
||||
// TODO: deserialize name representing class name, use the existing deserializeWith logic
|
||||
// to deserialize the class here. likely, deserialize name and then return a CommaSeparated
|
||||
// object
|
||||
}
|
||||
|
||||
deserializeString<T, V extends Visitor<T>>(visitor: V): T {
|
||||
const next = this.buffer.take()
|
||||
if (next.next() === Token.Quote) {
|
||||
let index = -1
|
||||
|
||||
do {
|
||||
index = this.buffer.indexOf(Token.Quote, index)
|
||||
} while (index > -1 && this.buffer.at(index - 1) === Token.ForwardSlash)
|
||||
|
||||
if (index === -1) {
|
||||
throw new SyntaxError('Unterminated string literal')
|
||||
}
|
||||
|
||||
const bytes = this.buffer.take(index)
|
||||
this.buffer.take()
|
||||
return visitor.visitString(bytes.toString())
|
||||
} else {
|
||||
throw unexpected('"', next.toString(), this.buffer.position)
|
||||
}
|
||||
}
|
||||
|
||||
deserializeNumber<T, V extends Visitor<T>>(visitor: V): T {
|
||||
const next = this.buffer.peek().next()
|
||||
|
||||
if (isNumericToken(next)) {
|
||||
const digits = this.buffer.takeWhile(isNumericToken).toString()
|
||||
if (digits.length >= 16) {
|
||||
const number = BigInt(digits)
|
||||
return visitor.visitBigInt(number)
|
||||
} else if (digits.length > 0) {
|
||||
let number = parseInt(digits.toString(), 10)
|
||||
return visitor.visitNumber(number)
|
||||
}
|
||||
}
|
||||
|
||||
throw unexpected('"-", ".", or 0..=9', next.toString(), this.buffer.position)
|
||||
}
|
||||
|
||||
deserializeBigInt<T, V extends Visitor<T>>(visitor: V): T {
|
||||
return this.deserializeNumber(visitor)
|
||||
}
|
||||
|
||||
deserializeBoolean<T, V extends Visitor<T>>(visitor: V): T {
|
||||
const next = this.buffer.next()
|
||||
let length = 3
|
||||
|
||||
switch (next) {
|
||||
case Literal.False[0]:
|
||||
length = 4
|
||||
case Literal.True[0]:
|
||||
break
|
||||
default:
|
||||
throw unexpected('"true" or "false"', this.buffer.next().toString(), this.buffer.position)
|
||||
}
|
||||
|
||||
this.buffer.take(length)
|
||||
return visitor.visitBoolean(length === 3)
|
||||
}
|
||||
|
||||
deserializeSymbol<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) {
|
||||
|
||||
const value = visitor.visitIterable!(new CommaSeparated(this))
|
||||
|
||||
next = this.buffer.take()
|
||||
if (next.next() === Token.RightSquare) {
|
||||
return value
|
||||
} else {
|
||||
throw unexpected(']', next.toString(), this.buffer.position)
|
||||
}
|
||||
} else {
|
||||
throw unexpected('[', next.toString(), this.buffer.position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,33 +1,37 @@
|
|||
import { IterableSerializer, ObjectSerializer, Serializable, Serializer } from './interface'
|
||||
import { Serde } from '../decorator'
|
||||
import { SerdeOptions, Stage } from '../options'
|
||||
import { ifNull, isFunction, isIterable, Nullable, orElse } from '../utils'
|
||||
import { IterableSerializer, Serializable, Serializer } from './interface'
|
||||
import { ifNull, isFunction, isIterable, isPlainObject, Nullable, orElse } from '../utils'
|
||||
|
||||
const unhandledType = (serializer: any, value: any) => new TypeError(`'${serializer.constructor.name}' has no method for value type '${typeof value}'`)
|
||||
|
||||
function serializeEntries<T, K extends string, V extends Serializable, E extends Iterable<[K, V]>>(serializer: Serializer<T>, value: E, options?: SerdeOptions) {
|
||||
function serializeEntries<T, K extends string, V extends Serializable, E extends Iterable<[K, V]>>(serializer: ObjectSerializer<T>, value: E, options?: SerdeOptions) {
|
||||
let state
|
||||
|
||||
const objectSerializer = serializer.serializeObject!()
|
||||
|
||||
for (const [key, val] of value) {
|
||||
if (options?.shouldSkipSerializing(key, val)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const name = options?.getPropertyName(key as string, Stage.Serialize) ?? key
|
||||
state = objectSerializer.serializeKey(name)
|
||||
state = objectSerializer.serializeValue(val)
|
||||
console.log('prop name for', key, name)
|
||||
state = serializer.serializeKey(name)
|
||||
state = serializer.serializeValue(val)
|
||||
}
|
||||
|
||||
return objectSerializer.end()
|
||||
return serializer.end()
|
||||
}
|
||||
|
||||
function serializeClass<T, K extends string, V extends Serializable, R extends Record<K, V>>(serializer: Serializer<T>, value: R, options?: SerdeOptions) {
|
||||
const classSerializer = serializer.serializeClass!(value.constructor.name)
|
||||
return serializeEntries(classSerializer, Object.entries(value) as Iterable<[K, V]>, options)
|
||||
}
|
||||
|
||||
function serializeObject<T, K extends string, V extends Serializable, R extends Record<K, V>>(serializer: Serializer<T>, value: R, options?: SerdeOptions) {
|
||||
return serializeEntries(serializer, Object.entries(value) as Iterable<[K, V]>, options)
|
||||
return serializeEntries(serializer.serializeObject!(), Object.entries(value) as Iterable<[K, V]>, options)
|
||||
}
|
||||
|
||||
function serializeIter<T, V extends Iterable<any>>(serializer: IterableSerializer<T>, value: V) {
|
||||
function serializeIter<T, V extends Iterable<any>>(serializer: IterableSerializer<T>, value: V, options?: SerdeOptions) {
|
||||
let state
|
||||
|
||||
for (const val of value) {
|
||||
|
@ -61,10 +65,13 @@ export function serializeWith<T>(serializer: Serializer<T>, value: Serializable,
|
|||
case 'undefined': return serialize(serializer.serializeNull)
|
||||
|
||||
case 'object':
|
||||
const options = optionsGetter(value)
|
||||
if (isIterable(value) && isFunction(serializer.serializeIterable)) {
|
||||
return serializeIter(serializer.serializeIterable(), value)
|
||||
return serializeIter(serializer.serializeIterable(), value, options)
|
||||
} if (!isPlainObject(value)) {
|
||||
return serializeClass(serializer, value as Record<PropertyKey, any>, options)
|
||||
} else if (isFunction(serializer.serializeObject)) {
|
||||
return serializeObject(serializer, value as Record<PropertyKey, any>, optionsGetter(value))
|
||||
return serializeObject(serializer, value as Record<PropertyKey, any>, options)
|
||||
} // deliberate fallthrough when the above fail
|
||||
|
||||
default: return serializeAny(value)
|
||||
|
|
|
@ -11,11 +11,6 @@ export interface IterableSerializer<T = void> {
|
|||
end(): T
|
||||
}
|
||||
|
||||
export interface ClassSerializer<T = void> {
|
||||
serializeField<U extends Serializable>(name: PropertyKey, value: U): T
|
||||
end(): T
|
||||
}
|
||||
|
||||
const TypeSerializerMethods = [
|
||||
'serializeString',
|
||||
'serializeNumber',
|
||||
|
@ -39,7 +34,7 @@ interface TypeSerializer<T> {
|
|||
serializeObject(): ObjectSerializer<T>
|
||||
// serializeMap?(): ObjectSerializer<T>
|
||||
serializeIterable?(): IterableSerializer<T>
|
||||
serializeClass(name: PropertyKey): ClassSerializer<T>
|
||||
serializeClass(name: PropertyKey): ObjectSerializer<T>
|
||||
}
|
||||
|
||||
const AnySerializerMethods = ['serializeAny']
|
||||
|
|
45
test.ts
Normal file
45
test.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
//import { register, serde } from './src'
|
||||
//import { CaseConvention } from './src/case'
|
||||
//import { deserialize } from './src/de'
|
||||
//import { fromString, toString } from './src/json'
|
||||
//import { serialize } from './src/ser'
|
||||
//import { Nullable } from './src/utils'
|
||||
//
|
||||
//@serialize
|
||||
//@deserialize
|
||||
//@register()
|
||||
//@serde({ renameAll: CaseConvention.PascalCase })
|
||||
//class InnerStruct {
|
||||
// private value: string
|
||||
//
|
||||
// @serde({ skip: true })
|
||||
// private metadata: any
|
||||
//
|
||||
// constructor(v: string) {
|
||||
// this.value = v
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//@serialize
|
||||
//@deserialize
|
||||
//@register()
|
||||
//@serde({ renameAll: CaseConvention.SnakeCase })
|
||||
//class TestStruct {
|
||||
// aNumber: number = 69
|
||||
// aString: Nullable<string>
|
||||
//
|
||||
// @serde({ skip: { serializing: { if: (v: boolean) => v } } })
|
||||
// aBoolean: boolean = false
|
||||
//
|
||||
// innerStruct: InnerStruct
|
||||
//
|
||||
// constructor(str: string) {
|
||||
// this.innerStruct = new InnerStruct(str)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//const test = new TestStruct('hi :3')
|
||||
//const ser = toString(test)
|
||||
//console.log(ser)
|
||||
//const de = fromString(ser, TestStruct)
|
||||
//console.log(de)
|
Loading…
Add table
Reference in a new issue