import { DefaultIterableAccessImpl, DefaultMapAccessImpl, Deserialize, Deserializer, IterableAccess, MapAccess, Visitor } from './de' 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>(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 { (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(DefaultMapAccessImpl) @mixin(DefaultIterableAccessImpl) export class CommaSeparated implements MapAccess, IterableAccess { private readonly de: JSONDeserializer private first: boolean = true constructor(deserializer: JSONDeserializer) { this.de = deserializer } nextKeySeed>(seed: K): Nullable { 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>(seed: V): Nullable { const next = this.de.buffer.next() if (next !== Token.Colon) { throw unexpected(':', next.toString(), this.de.buffer.position) } return seed.deserialize(this.de) } nextElementSeed>(seed: I): Nullable { 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): 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 { private ser: JSONSerializer private first: boolean = true constructor(serializer: JSONSerializer) { this.ser = serializer serializer.write('{') } serializeKey(key: U): void { if (!this.first) { this.ser.write(',') } else { this.first = false } serializeWith(this.ser, key) } serializeValue(value: U): void { this.ser.write(':') serializeWith(this.ser, value) } end(): void { this.ser.write('}') } } class JSONIterableSerializer implements IterableSerializer { private ser: JSONSerializer private first: boolean = true constructor(serializer: JSONSerializer) { this.ser = serializer serializer.write('[') } serializeElement(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 { 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 { return new JSONObjectSerializer(this) } serializeNumber(value: number) { this.write(value.toString()) } serializeBigInt(value: bigint) { this.write(value.toString()) } serializeIterable(): IterableSerializer { 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 { readonly buffer: StringBuffer constructor(buffer: StringBuffer) { this.buffer = buffer } static fromString(value: string): JSONDeserializer { return new this(StringBuffer.fromString(value)) } deserializeAny>(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>(visitor: V): T { if (this.buffer.startsWith('null')) { this.buffer.take(4) } return visitor.visitNull() } deserializeObject>(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) } } deserializeString>(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>(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>(visitor: V): T { return this.deserializeNumber(visitor) } deserializeBoolean>(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>(_visitor: V): T { throw new Error('Method not implemented.') } deserializeIterable?>(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) } } }