use json.stringify/parse

This commit is contained in:
Rowan 2025-05-25 11:04:05 -05:00
parent 7c4962691c
commit 75cbc66063
7 changed files with 1185 additions and 1534 deletions

1023
dist/index.js vendored Normal file

File diff suppressed because it is too large Load diff

1101
dist/test.js vendored

File diff suppressed because it is too large Load diff

18
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"kuebiko": "file:../kuebiko/",
"serde": "file:../serde-ts" "serde": "file:../serde-ts"
}, },
"devDependencies": { "devDependencies": {
@ -16,6 +17,19 @@
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
}, },
"../kuebiko": {
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"izuna": "git+https://git.kitsu.cafe/rowan/izuna.git",
"kojima": "git+https://git.kitsu.cafe/rowan/kojima.git"
},
"devDependencies": {
"esbuild": "^0.25.2",
"folktest": "git+https://git.kitsu.cafe/rowan/folktest.git",
"typescript": "^5.8.3"
}
},
"../serde-ts": { "../serde-ts": {
"name": "serde", "name": "serde",
"version": "1.0.0", "version": "1.0.0",
@ -494,6 +508,10 @@
"@esbuild/win32-x64": "0.25.4" "@esbuild/win32-x64": "0.25.4"
} }
}, },
"node_modules/kuebiko": {
"resolved": "../kuebiko",
"link": true
},
"node_modules/serde": { "node_modules/serde": {
"resolved": "../serde-ts", "resolved": "../serde-ts",
"link": true "link": true

View file

@ -3,6 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/index.js",
"type": "module",
"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 --format=cjs --bundle --target=es2022 --outfile=dist/test.js --tsconfig=tsconfig.json",
@ -11,7 +12,6 @@
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"type": "commonjs",
"dependencies": { "dependencies": {
"serde": "file:../serde-ts" "serde": "file:../serde-ts"
}, },

435
src/de.ts
View file

@ -1,396 +1,135 @@
import { GlobalRegistry, IterResult, Registry } from 'serde' import { getDeserialize, GlobalRegistry, IterResult, Registry } from 'serde'
import { IIterableAccess, MapAccess, IVisitor, IDeserializer, Deserialize, GenericSeed, Visitor } from 'serde/de' import { Deserialize, Forwarder, IDeserializer, IterableAccess, IVisitor, MapAccess, Visitor } from 'serde/de'
import { unexpected } from './err'
type Byte = number interface Reviver {
<T, U>(key: string, value: T): U
const clamp = (value: number, min: number, max: number): number => {
return Math.min(Math.max(value, min), max)
} }
const isNumeric = (value: any): value is number => { type Entry = [any, any]
return !isNaN(value)
const unwrap = (de: IDeserializer) => de.deserializeAny(new Visitor())
const deserializer = (registry: Registry = GlobalRegistry) => <T, U>(_key: string, value: T) => {
const de = getDeserialize(value, unwrap, registry)
return de(new Forwarder(value)) as U
} }
const isNumericToken = (value: Byte) => { const parser = (reviver: Reviver) => <T, U>(value: T): U => {
return value === Token.Period || value === Token.Hyphen || Token.Digit.includes(value) return JSON.parse(value as any, reviver)
} }
interface Predicate<T> { export class JSONMapAccess extends MapAccess {
(value: T): boolean private readonly _entries: Entry[]
} private index: number = -1
const encoder = new TextEncoder() constructor(entries: Entry[]) {
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 class CommaSeparated<T> extends MapAccess implements IIterableAccess {
private readonly de: JSONDeserializer
private readonly defaultSeed: GenericSeed<any>
private first: boolean = true
constructor(deserializer: JSONDeserializer, visitor: IVisitor<T> = new Visitor()) {
super() super()
this.de = deserializer
this.defaultSeed = new GenericSeed(visitor) this._entries = entries
} }
private seed<V>(): Deserialize<V> { static fromObject(value: object) {
return this.defaultSeed.deserialize.bind(this.defaultSeed) as Deserialize<V> return new this(Object.entries(value))
} }
private nextItemSeed<T, D extends Deserialize<T>>(seed: D, end: number): IteratorResult<T> { nextKey<T>(seed?: Deserialize<T>): IteratorResult<T> {
if (this.de.buffer.peek().next() === end) { this.index += 1
if (this.index >= this._entries.length) {
return IterResult.Done() return IterResult.Done()
} else {
const key = this._entries[this.index][0]
const deser = seed != null ? seed(key) : key
return IterResult.Next(deser)
} }
}
if (!this.first) { nextValue<T>(seed?: Deserialize<T>): IteratorResult<T> {
const take = this.de.buffer.take() if (this.index >= this._entries.length) {
if (take.next() !== Token.Comma) { return IterResult.Done()
throw unexpected(',', take.toString(), this.de.buffer.position) } else {
} const value = this._entries[this.index][1]
const deser = seed != null ? seed(value) : value
return IterResult.Next(deser)
} }
this.first = false
return IterResult.Next(seed(this.de)) as IteratorResult<T>
}
nextKeySeed<T, K extends Deserialize<T>>(seed: K): IteratorResult<T> {
return this.nextItemSeed(seed, Token.RightCurly)
}
nextValueSeed<T, V extends Deserialize<T>>(seed: V): IteratorResult<T> {
const next = this.de.buffer.next()
if (next !== Token.Colon) {
throw unexpected(':', String.fromCharCode(next), this.de.buffer.position)
}
return IterResult.Next(seed(this.de)) as IteratorResult<T>
}
private nextItem<T>(end: number): IteratorResult<T> {
return this.nextItemSeed(this.seed<T>(), end)
}
nextKey<T>(): IteratorResult<T> {
return this.nextItem(Token.RightCurly)
}
nextValue<V>(): IteratorResult<V> {
return this.nextValueSeed(this.seed<V>())
}
nextElement<T>(): IteratorResult<T> {
return this.nextItem(Token.RightSquare)
} }
} }
class ByteArray { export class JSONIterableAccess extends IterableAccess {
private readonly view: Uint8Array private readonly iterator: Iterator<any>
private readonly encoder: TextEncoder
private readonly decoder: TextDecoder
private index: number = 0 constructor(iterator: Iterator<any>) {
super()
get position() { this.iterator = iterator
return this.index
} }
get length() { static fromIterable(iterable: Iterable<any>) {
return this.view.byteLength return new this(iterable[Symbol.iterator]())
} }
constructor(view: Uint8Array, encoder: TextEncoder = new TextEncoder(), decoder: TextDecoder = new TextDecoder()) { nextElement<T, D extends Deserialize<T>>(seed?: D): IteratorResult<T> {
this.view = view const result = this.iterator.next()
this.encoder = encoder if (result.done) {
this.decoder = decoder return IterResult.Done()
} } else {
const value = seed != null ? seed(result.value) : result.value
static fromArrayBuffer(value: ArrayBuffer, encoder?: TextEncoder, decoder?: TextDecoder): ByteArray { return IterResult.Next(value)
return new this(new Uint8Array(value), encoder, decoder)
}
static fromString(value: string, encoder: TextEncoder = new TextEncoder(), decoder?: TextDecoder): ByteArray {
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): ByteArray {
const bytes = this.peek(limit)
this.index += limit
return bytes
}
at(index: number) {
return this.view[this.index + index]
}
takeWhile(fn: Predicate<number>): ByteArray {
let index = 0
while (!this.done() && fn(this.at(index))) {
index += 1
} }
return this.take(index)
}
takeUntil(fn: Predicate<number>): ByteArray {
return this.takeWhile((v: number) => !fn(v))
}
drop(limit: number) {
this.index += limit
return this
}
dropWhile(fn: Predicate<number>): ByteArray {
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
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 ByteArray(
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
} }
} }
export class JSONDeserializer implements IDeserializer { export class JSONDeserializer implements IDeserializer {
readonly buffer: ByteArray private readonly input: string
readonly registry: Registry private readonly parser: <T, U>(value: T) => U
constructor(buffer: ByteArray, registry: Registry = GlobalRegistry) { constructor(input: string, registry: Registry = GlobalRegistry) {
this.buffer = buffer this.input = input
this.registry = registry this.parser = parser(deserializer(registry))
} }
static fromString(value: string): JSONDeserializer { static fromString(value: string, registry?: Registry) {
return new this(ByteArray.fromString(value)) return new this(value, registry)
} }
deserializeAny<T>(visitor: IVisitor<T>): T { deserializeAny<T>(_visitor: IVisitor<T>): T {
const peek = this.buffer.peek() return this.parser(this.input)
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 IVisitor<T>>(visitor: V): T { deserializeBoolean<T>(visitor: IVisitor<T>): T {
if (this.buffer.startsWith('null')) { return visitor.visitBoolean(this.parser(this.input))
this.buffer.take(4) }
}
deserializeNumber<T>(visitor: IVisitor<T>): T {
return visitor.visitNumber(this.parser(this.input))
}
deserializeBigInt<T>(visitor: IVisitor<T>): T {
return visitor.visitBigInt(this.parser(this.input))
}
deserializeString<T>(visitor: IVisitor<T>): T {
return visitor.visitString(this.parser(this.input))
}
deserializeSymbol<T>(visitor: IVisitor<T>): T {
return visitor.visitSymbol(this.parser(this.input))
}
deserializeNull<T>(visitor: IVisitor<T>): T {
return visitor.visitNull() return visitor.visitNull()
} }
deserializeObject<T, V extends IVisitor<T>>(visitor: V): T { deserializeObject<T>(visitor: IVisitor<T>): T {
let next = this.buffer.take() const value = this.parser(this.input) as object
if (next.next() === Token.LeftCurly) { return visitor.visitObject(JSONMapAccess.fromObject(value))
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 IVisitor<T>>(_name: string, visitor: V): T { deserializeIterable<T>(visitor: IVisitor<T>): T {
return this.deserializeObject(visitor) const value = this.parser(this.input) as Iterable<any>
return visitor.visitIterable(JSONIterableAccess.fromIterable(value))
} }
deserializeString<T, V extends IVisitor<T>>(visitor: V): T { deserializeFunction<T>(_visitor: IVisitor<T>): T {
const next = this.buffer.take() throw new Error('Method not implemented')
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 IVisitor<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 IVisitor<T>>(visitor: V): T {
return this.deserializeNumber(visitor)
}
deserializeBoolean<T, V extends IVisitor<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 IVisitor<T>>(_visitor: V): T {
throw new Error('Method not implemented.')
}
deserializeIterable<T, V extends IVisitor<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)
}
}
deserializeFunction<T, V extends IVisitor<T>>(_visitor: V): T {
throw new Error('Method not implemented.')
}
} }

View file

@ -5,8 +5,7 @@ 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) return serialize(serializer, value)
return serializer.output
} }
export function fromString<T>(value: string, into: any): T { export function fromString<T>(value: string, into: any): T {

View file

@ -1,106 +1,79 @@
import { ISerializeIterable, ISerializeObject, ISerializer, serialize, SerializeObject } from "serde/ser" import { getSerialize, GlobalRegistry, Registry } from 'serde'
import { IdentitySerializer, ISerializer, serialize, SerializeIterable, SerializeObject, Serializer } from 'serde/ser'
class JSONObjectSerializer extends SerializeObject<void> { interface Stringify {
private ser: JSONSerializer (value: any): string
private first: boolean = true }
constructor(serializer: JSONSerializer) { type Replacer = (key: PropertyKey, value: any) => any
class JSONSerializeObject extends SerializeObject<string> {
private readonly stringify: Stringify
private value: Record<PropertyKey, any> = {}
private currentKey?: string
constructor(stringify: Stringify) {
super() super()
this.ser = serializer this.stringify = stringify
serializer.write('{')
} }
serializeKey<U>(key: U): void { serializeKey(key: string): void {
if (!this.first) { this.currentKey = key
this.ser.write(',')
} else {
this.first = false
}
serialize(this.ser, key)
this.ser.write(':')
} }
serializeValue<U>(value: U): void { serializeValue<U>(value: U): void {
serialize(this.ser, value) this.value[this.currentKey!] = value
this.currentKey = undefined
} }
end(): void { end(): string {
this.ser.write('}') return this.stringify(this.value)
} }
} }
class JSONIterableSerializer implements ISerializeIterable<void> { class JSONSerializeIterable extends SerializeIterable<string> {
private ser: JSONSerializer private readonly stringify: Stringify
private first: boolean = true private elements: any[] = []
constructor(serializer: JSONSerializer) { constructor(stringify: Stringify) {
this.ser = serializer super()
serializer.write('[') this.stringify = stringify
} }
serializeElement<U>(element: U): void { serializeElement<U>(value: U): void {
if (!this.first) { this.elements.push(value)
this.ser.write(',')
} else {
this.first = false
}
serialize(this.ser, element)
} }
end(): void { end(): string {
this.ser.write(']') return this.stringify(this.elements)
} }
} }
export class JSONSerializer implements ISerializer<void> { const id = <T, U>(_ser: ISerializer<T>, value: U) => value
output: string = '' const serializer = (registry: Registry) => <T>(_key: PropertyKey, value: T) => {
const ser = getSerialize(value, id, registry)
write(value: string) { return ser(new IdentitySerializer(), value)
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(): ISerializeObject<void> {
return new JSONObjectSerializer(this)
}
serializeClass(_name: PropertyKey): ISerializeObject<void> {
return this.serializeObject()
}
serializeNumber(value: number) {
this.write(value.toString())
}
serializeBigInt(value: bigint) {
this.write(value.toString())
}
serializeIterable(): ISerializeIterable<void> {
return new JSONIterableSerializer(this)
}
serializeNull() {
return this.write('null')
}
} }
const stringify = (replacer: Replacer) => (value: any) => JSON.stringify(value, replacer)
export class JSONSerializer extends Serializer<string> {
private stringify: (value: any) => string
constructor(registry: Registry = GlobalRegistry) {
super()
this.stringify = stringify(serializer(registry))
}
serializeAny(value: any) { return this.stringify(value) }
serializeBoolean(value: any) { return JSON.stringify(value) }
serializeNumber(value: any) { return JSON.stringify(value) }
serializeBigInt(value: any) { return JSON.stringify(value) }
serializeString(value: any) { return JSON.stringify(value) }
serializeSymbol(value: any) { return JSON.stringify(value) }
serializeNull() { return JSON.stringify(null) }
serializeIterable(_len: number) { return new JSONSerializeIterable(this.stringify) }
serializeObject(_len: number) { return new JSONSerializeObject(this.stringify) }
serializeClass(_name: string, _len: number) { return new JSONSerializeObject(this.stringify) }
}