Compare commits
No commits in common. "0b100c5b9e95af6a33db0c4a29635973df00c746" and "981e74c3ccc4b1282d5683816488defbb501d7bd" have entirely different histories.
0b100c5b9e
...
981e74c3cc
5 changed files with 72 additions and 1124 deletions
1015
dist/test.js
vendored
1015
dist/test.js
vendored
File diff suppressed because it is too large
Load diff
|
@ -2,10 +2,10 @@
|
||||||
"name": "serde-json-ts",
|
"name": "serde-json-ts",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "esbuild src/index.ts --bundle --outfile=dist/index.js",
|
"build": "esbuild src/index.ts --bundle --outfile=dist/index.js",
|
||||||
"build:test": "esbuild test.ts --format=cjs --bundle --target=es2022 --outfile=dist/test.js --tsconfig=tsconfig.json",
|
"build:test": "esbuild test.ts --platform=node --target=es2022 --bundle --outfile=dist/test.js --tsconfig=tsconfig.json",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|
138
src/de.ts
138
src/de.ts
|
@ -1,6 +1,6 @@
|
||||||
import { IIterableAccess, MapAccess, IVisitor, IDeserializer, IterResult, Deserialize, GenericVisitor, GenericSeed } from 'serde/de'
|
import { DefaultIterableAccessImpl, DefaultMapAccessImpl, Deserialize, Deserializer, GenericVisitor, IterableAccess, MapAccess, Visitor } from 'serde/de'
|
||||||
|
import { mixin, Nullable } from 'serde/utils'
|
||||||
import { unexpected } from './err'
|
import { unexpected } from './err'
|
||||||
import { GlobalRegistry, Registry } from 'serde'
|
|
||||||
|
|
||||||
type Byte = number
|
type Byte = number
|
||||||
|
|
||||||
|
@ -44,24 +44,20 @@ const Token = Object.freeze({
|
||||||
Colon: char`:`
|
Colon: char`:`
|
||||||
} as const)
|
} as const)
|
||||||
|
|
||||||
export class CommaSeparated<T> extends MapAccess implements IIterableAccess {
|
export interface CommaSeparated extends MapAccess, IterableAccess { }
|
||||||
|
@mixin<MapAccess>(DefaultMapAccessImpl)
|
||||||
|
@mixin<IterableAccess>(DefaultIterableAccessImpl)
|
||||||
|
export class CommaSeparated implements MapAccess, IterableAccess {
|
||||||
private readonly de: JSONDeserializer
|
private readonly de: JSONDeserializer
|
||||||
private readonly defaultSeed: GenericSeed<T>
|
|
||||||
private first: boolean = true
|
private first: boolean = true
|
||||||
|
|
||||||
constructor(deserializer: JSONDeserializer, visitor: IVisitor<T> = new GenericVisitor()) {
|
constructor(deserializer: JSONDeserializer) {
|
||||||
super()
|
|
||||||
this.de = deserializer
|
this.de = deserializer
|
||||||
this.defaultSeed = new GenericSeed(visitor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private seed(): Deserialize {
|
nextKeySeed<T, K extends Deserialize<T>>(seed: K): Nullable<T> {
|
||||||
return this.defaultSeed.deserialize.bind(this.defaultSeed) as Deserialize
|
if (this.de.buffer.peek().next() === Token.RightCurly) {
|
||||||
}
|
return
|
||||||
|
|
||||||
private nextItemSeed<T, D extends Deserialize>(seed: D, end: number): IteratorResult<T> {
|
|
||||||
if (this.de.buffer.peek().next() === end) {
|
|
||||||
return IterResult.Done()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.first) {
|
if (!this.first) {
|
||||||
|
@ -72,47 +68,42 @@ export class CommaSeparated<T> extends MapAccess implements IIterableAccess {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.first = false
|
this.first = false
|
||||||
return IterResult.Next(seed(this.de)) as IteratorResult<T>
|
return seed.deserialize(this.de)
|
||||||
}
|
}
|
||||||
|
|
||||||
nextKeySeed<T, K extends Deserialize>(seed: K): IteratorResult<T> {
|
nextValueSeed<T, V extends Deserialize<T>>(seed: V): Nullable<T> {
|
||||||
return this.nextItemSeed(seed, Token.RightCurly)
|
|
||||||
}
|
|
||||||
|
|
||||||
nextValueSeed<T, V extends Deserialize>(seed: V): IteratorResult<T> {
|
|
||||||
const next = this.de.buffer.next()
|
const next = this.de.buffer.next()
|
||||||
|
|
||||||
if (next !== Token.Colon) {
|
if (next !== Token.Colon) {
|
||||||
throw unexpected(':', String.fromCharCode(next), this.de.buffer.position)
|
console.log(this.de.buffer.toString())
|
||||||
|
throw unexpected(':', next.toString(), this.de.buffer.position)
|
||||||
}
|
}
|
||||||
|
|
||||||
return IterResult.Next(seed(this.de)) as IteratorResult<T>
|
return seed.deserialize(this.de)
|
||||||
}
|
}
|
||||||
|
|
||||||
private nextItem<T>(end: number): IteratorResult<T> {
|
nextElementSeed<T, I extends Deserialize<T>>(seed: I): Nullable<T> {
|
||||||
return this.nextItemSeed(this.seed(), end)
|
if (this.de.buffer.peek().next() === Token.RightSquare) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nextKey<T>(): IteratorResult<T> {
|
if (!this.first) {
|
||||||
return this.nextItem(Token.RightCurly)
|
const take = this.de.buffer.take()
|
||||||
}
|
if (take.next() !== Token.Comma) {
|
||||||
|
throw unexpected(',', take.toString(), this.de.buffer.position)
|
||||||
nextValue<V>(): IteratorResult<V> {
|
|
||||||
return this.nextValueSeed(this.seed())
|
|
||||||
}
|
|
||||||
|
|
||||||
nextElement<T>(): IteratorResult<T> {
|
|
||||||
return this.nextItem(Token.RightSquare)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ByteArray {
|
this.first = false
|
||||||
|
return seed.deserialize(this.de)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringBuffer {
|
||||||
private readonly view: Uint8Array
|
private readonly view: Uint8Array
|
||||||
|
private index: number = 0
|
||||||
private readonly encoder: TextEncoder
|
private readonly encoder: TextEncoder
|
||||||
private readonly decoder: TextDecoder
|
private readonly decoder: TextDecoder
|
||||||
|
|
||||||
private index: number = 0
|
|
||||||
|
|
||||||
get position() {
|
get position() {
|
||||||
return this.index
|
return this.index
|
||||||
}
|
}
|
||||||
|
@ -127,11 +118,11 @@ class ByteArray {
|
||||||
this.decoder = decoder
|
this.decoder = decoder
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromArrayBuffer(value: ArrayBuffer, encoder?: TextEncoder, decoder?: TextDecoder): ByteArray {
|
static fromArrayBuffer(value: ArrayBuffer, encoder?: TextEncoder, decoder?: TextDecoder): StringBuffer {
|
||||||
return new this(new Uint8Array(value), encoder, decoder)
|
return new this(new Uint8Array(value), encoder, decoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromString(value: string, encoder: TextEncoder = new TextEncoder(), decoder?: TextDecoder): ByteArray {
|
static fromString(value: string, encoder: TextEncoder = new TextEncoder(), decoder?: TextDecoder): StringBuffer {
|
||||||
return this.fromArrayBuffer(
|
return this.fromArrayBuffer(
|
||||||
encoder.encode(value),
|
encoder.encode(value),
|
||||||
encoder,
|
encoder,
|
||||||
|
@ -161,7 +152,7 @@ class ByteArray {
|
||||||
return this.decoder.decode(this.toBytes())
|
return this.decoder.decode(this.toBytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
take(limit: number = 1): ByteArray {
|
take(limit: number = 1): StringBuffer {
|
||||||
const bytes = this.peek(limit)
|
const bytes = this.peek(limit)
|
||||||
this.index += limit
|
this.index += limit
|
||||||
return bytes
|
return bytes
|
||||||
|
@ -171,7 +162,7 @@ class ByteArray {
|
||||||
return this.view[this.index + index]
|
return this.view[this.index + index]
|
||||||
}
|
}
|
||||||
|
|
||||||
takeWhile(fn: Predicate<number>): ByteArray {
|
takeWhile(fn: Predicate<number>): StringBuffer {
|
||||||
let index = 0
|
let index = 0
|
||||||
|
|
||||||
while (!this.done() && fn(this.at(index))) {
|
while (!this.done() && fn(this.at(index))) {
|
||||||
|
@ -181,30 +172,12 @@ class ByteArray {
|
||||||
return this.take(index)
|
return this.take(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
takeUntil(fn: Predicate<number>): ByteArray {
|
|
||||||
return this.takeWhile((v: number) => !fn(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(limit: number) {
|
drop(limit: number) {
|
||||||
this.index += limit
|
this.index += limit
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
dropWhile(fn: Predicate<number>): ByteArray {
|
peek(limit: number = 1): StringBuffer {
|
||||||
let index = 0
|
|
||||||
|
|
||||||
while (!this.done() && fn(this.at(index))) {
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.drop(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
dropUntil(fn: Predicate<number>): ByteArray {
|
|
||||||
return this.dropWhile((v: number) => !fn(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
peek(limit: number = 1): ByteArray {
|
|
||||||
const index = this.index
|
const index = this.index
|
||||||
return this.slice(index, index + limit)
|
return this.slice(index, index + limit)
|
||||||
}
|
}
|
||||||
|
@ -221,7 +194,7 @@ class ByteArray {
|
||||||
}
|
}
|
||||||
|
|
||||||
slice(start?: number, end?: number) {
|
slice(start?: number, end?: number) {
|
||||||
return new ByteArray(
|
return new StringBuffer(
|
||||||
this.view.subarray(start, end),
|
this.view.subarray(start, end),
|
||||||
this.encoder,
|
this.encoder,
|
||||||
this.decoder
|
this.decoder
|
||||||
|
@ -243,20 +216,18 @@ class ByteArray {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JSONDeserializer implements IDeserializer {
|
export class JSONDeserializer implements Deserializer {
|
||||||
readonly buffer: ByteArray
|
readonly buffer: StringBuffer
|
||||||
readonly registry: Registry
|
|
||||||
|
|
||||||
constructor(buffer: ByteArray, registry: Registry = GlobalRegistry) {
|
constructor(buffer: StringBuffer) {
|
||||||
this.buffer = buffer
|
this.buffer = buffer
|
||||||
this.registry = registry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromString(value: string): JSONDeserializer {
|
static fromString(value: string): JSONDeserializer {
|
||||||
return new this(ByteArray.fromString(value))
|
return new this(StringBuffer.fromString(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializeAny<T>(visitor: IVisitor<T>): T {
|
deserializeAny<T, V extends Visitor<T>>(visitor: V): T {
|
||||||
const peek = this.buffer.peek()
|
const peek = this.buffer.peek()
|
||||||
const nextByte = peek.take()
|
const nextByte = peek.take()
|
||||||
const byte = nextByte.next()
|
const byte = nextByte.next()
|
||||||
|
@ -279,7 +250,7 @@ export class JSONDeserializer implements IDeserializer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializeNull<T, V extends IVisitor<T>>(visitor: V): T {
|
deserializeNull<T, V extends Visitor<T>>(visitor: V): T {
|
||||||
if (this.buffer.startsWith('null')) {
|
if (this.buffer.startsWith('null')) {
|
||||||
this.buffer.take(4)
|
this.buffer.take(4)
|
||||||
}
|
}
|
||||||
|
@ -287,11 +258,11 @@ export class JSONDeserializer implements IDeserializer {
|
||||||
return visitor.visitNull()
|
return visitor.visitNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializeObject<T, V extends IVisitor<T>>(visitor: V): T {
|
private _deserializeObject<T>(fn: (value: CommaSeparated) => T): T {
|
||||||
let next = this.buffer.take()
|
let next = this.buffer.take()
|
||||||
if (next.next() === Token.LeftCurly) {
|
if (next.next() === Token.LeftCurly) {
|
||||||
|
|
||||||
const value = visitor.visitObject(new CommaSeparated(this))
|
const value = fn(new CommaSeparated(this))
|
||||||
|
|
||||||
next = this.buffer.take()
|
next = this.buffer.take()
|
||||||
if (next.next() === Token.RightCurly) {
|
if (next.next() === Token.RightCurly) {
|
||||||
|
@ -304,11 +275,11 @@ export class JSONDeserializer implements IDeserializer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializeClass<T, V extends IVisitor<T>>(_name: string, visitor: V): T {
|
deserializeObject<T, V extends Visitor<T>>(visitor: V): T {
|
||||||
return this.deserializeObject(visitor)
|
return this._deserializeObject(visitor.visitObject.bind(visitor))
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializeString<T, V extends IVisitor<T>>(visitor: V): T {
|
deserializeString<T, V extends Visitor<T>>(visitor: V): T {
|
||||||
const next = this.buffer.take()
|
const next = this.buffer.take()
|
||||||
if (next.next() === Token.Quote) {
|
if (next.next() === Token.Quote) {
|
||||||
let index = -1
|
let index = -1
|
||||||
|
@ -329,7 +300,7 @@ export class JSONDeserializer implements IDeserializer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializeNumber<T, V extends IVisitor<T>>(visitor: V): T {
|
deserializeNumber<T, V extends Visitor<T>>(visitor: V): T {
|
||||||
const next = this.buffer.peek().next()
|
const next = this.buffer.peek().next()
|
||||||
|
|
||||||
if (isNumericToken(next)) {
|
if (isNumericToken(next)) {
|
||||||
|
@ -346,11 +317,11 @@ export class JSONDeserializer implements IDeserializer {
|
||||||
throw unexpected('"-", ".", or 0..=9', next.toString(), this.buffer.position)
|
throw unexpected('"-", ".", or 0..=9', next.toString(), this.buffer.position)
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializeBigInt<T, V extends IVisitor<T>>(visitor: V): T {
|
deserializeBigInt<T, V extends Visitor<T>>(visitor: V): T {
|
||||||
return this.deserializeNumber(visitor)
|
return this.deserializeNumber(visitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializeBoolean<T, V extends IVisitor<T>>(visitor: V): T {
|
deserializeBoolean<T, V extends Visitor<T>>(visitor: V): T {
|
||||||
const next = this.buffer.next()
|
const next = this.buffer.next()
|
||||||
let length = 3
|
let length = 3
|
||||||
|
|
||||||
|
@ -367,15 +338,15 @@ export class JSONDeserializer implements IDeserializer {
|
||||||
return visitor.visitBoolean(length === 3)
|
return visitor.visitBoolean(length === 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializeSymbol<T, V extends IVisitor<T>>(_visitor: V): T {
|
deserializeSymbol<T, V extends Visitor<T>>(_visitor: V): T {
|
||||||
throw new Error('Method not implemented.')
|
throw new Error('Method not implemented.')
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializeIterable<T, V extends IVisitor<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) {
|
||||||
|
|
||||||
const value = visitor.visitIterable(new CommaSeparated(this))
|
const value = visitor.visitIterable!(new CommaSeparated(this))
|
||||||
|
|
||||||
next = this.buffer.take()
|
next = this.buffer.take()
|
||||||
if (next.next() === Token.RightSquare) {
|
if (next.next() === Token.RightSquare) {
|
||||||
|
@ -387,10 +358,5 @@ export class JSONDeserializer implements IDeserializer {
|
||||||
throw unexpected('[', next.toString(), this.buffer.position)
|
throw unexpected('[', next.toString(), this.buffer.position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializeFunction<T, V extends IVisitor<T>>(_visitor: V): T {
|
|
||||||
throw new Error('Method not implemented.')
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
|
import { serializeWith } from 'serde/ser'
|
||||||
|
import { Deserialize } from 'serde/de'
|
||||||
import { JSONSerializer } from './ser'
|
import { JSONSerializer } from './ser'
|
||||||
import { deserialize, Deserialize } from 'serde/de'
|
|
||||||
import { JSONDeserializer } from './de'
|
import { JSONDeserializer } from './de'
|
||||||
import { serialize } from 'serde/ser'
|
|
||||||
|
|
||||||
export function toString(value: any): string {
|
export function toString(value: any): string {
|
||||||
const serializer = new JSONSerializer()
|
const serializer = new JSONSerializer()
|
||||||
serialize(serializer, value)
|
serializeWith(serializer, value)
|
||||||
return serializer.output
|
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 {
|
||||||
const deserializer = JSONDeserializer.fromString(value)
|
const deserializer = JSONDeserializer.fromString(value)
|
||||||
return deserialize(deserializer, into)
|
return into.deserialize(deserializer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
31
src/ser.ts
31
src/ser.ts
|
@ -1,28 +1,29 @@
|
||||||
import { ISerializeIterable, ISerializeObject, ISerializer, serialize, SerializeObject } from "serde/ser"
|
import { IterableSerializer, ObjectSerializer, Serializable, Serializer, serializeWith } from 'serde/ser'
|
||||||
|
|
||||||
class JSONObjectSerializer extends SerializeObject<void> {
|
const Identifier = (value: string) => `\x02${value}\x04`
|
||||||
|
|
||||||
|
class JSONObjectSerializer implements ObjectSerializer<void> {
|
||||||
private ser: JSONSerializer
|
private ser: JSONSerializer
|
||||||
private first: boolean = true
|
private first: boolean = true
|
||||||
|
|
||||||
constructor(serializer: JSONSerializer) {
|
constructor(serializer: JSONSerializer) {
|
||||||
super()
|
|
||||||
this.ser = serializer
|
this.ser = serializer
|
||||||
serializer.write('{')
|
serializer.write('{')
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeKey<U>(key: U): void {
|
serializeKey<U extends Serializable>(key: U): void {
|
||||||
if (!this.first) {
|
if (!this.first) {
|
||||||
this.ser.write(',')
|
this.ser.write(',')
|
||||||
} else {
|
} else {
|
||||||
this.first = false
|
this.first = false
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(this.ser, key)
|
serializeWith(this.ser, key)
|
||||||
this.ser.write(':')
|
this.ser.write(':')
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeValue<U>(value: U): void {
|
serializeValue<U extends Serializable>(value: U): void {
|
||||||
serialize(this.ser, value)
|
serializeWith(this.ser, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
end(): void {
|
end(): void {
|
||||||
|
@ -30,7 +31,7 @@ class JSONObjectSerializer extends SerializeObject<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JSONIterableSerializer implements ISerializeIterable<void> {
|
class JSONIterableSerializer implements IterableSerializer<void> {
|
||||||
private ser: JSONSerializer
|
private ser: JSONSerializer
|
||||||
private first: boolean = true
|
private first: boolean = true
|
||||||
|
|
||||||
|
@ -39,14 +40,14 @@ class JSONIterableSerializer implements ISerializeIterable<void> {
|
||||||
serializer.write('[')
|
serializer.write('[')
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeElement<U>(element: U): void {
|
serializeElement<U extends Serializable>(element: U): void {
|
||||||
if (!this.first) {
|
if (!this.first) {
|
||||||
this.ser.write(',')
|
this.ser.write(',')
|
||||||
} else {
|
} else {
|
||||||
this.first = false
|
this.first = false
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(this.ser, element)
|
serializeWith(this.ser, element)
|
||||||
}
|
}
|
||||||
|
|
||||||
end(): void {
|
end(): void {
|
||||||
|
@ -54,7 +55,7 @@ class JSONIterableSerializer implements ISerializeIterable<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JSONSerializer implements ISerializer<void> {
|
export class JSONSerializer implements Serializer<void> {
|
||||||
output: string = ''
|
output: string = ''
|
||||||
|
|
||||||
write(value: string) {
|
write(value: string) {
|
||||||
|
@ -78,14 +79,10 @@ export class JSONSerializer implements ISerializer<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeObject(): ISerializeObject<void> {
|
serializeObject(): ObjectSerializer<void> {
|
||||||
return new JSONObjectSerializer(this)
|
return new JSONObjectSerializer(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeClass(_name: PropertyKey): ISerializeObject<void> {
|
|
||||||
return this.serializeObject()
|
|
||||||
}
|
|
||||||
|
|
||||||
serializeNumber(value: number) {
|
serializeNumber(value: number) {
|
||||||
this.write(value.toString())
|
this.write(value.toString())
|
||||||
}
|
}
|
||||||
|
@ -94,7 +91,7 @@ export class JSONSerializer implements ISerializer<void> {
|
||||||
this.write(value.toString())
|
this.write(value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeIterable(): ISerializeIterable<void> {
|
serializeIterable(): IterableSerializer<void> {
|
||||||
return new JSONIterableSerializer(this)
|
return new JSONIterableSerializer(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue