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",
"license": "ISC",
"dependencies": {
"kuebiko": "file:../kuebiko/",
"serde": "file:../serde-ts"
},
"devDependencies": {
@ -16,6 +17,19 @@
"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": {
"name": "serde",
"version": "1.0.0",
@ -494,6 +508,10 @@
"@esbuild/win32-x64": "0.25.4"
}
},
"node_modules/kuebiko": {
"resolved": "../kuebiko",
"link": true
},
"node_modules/serde": {
"resolved": "../serde-ts",
"link": true

View file

@ -3,6 +3,7 @@
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"type": "module",
"scripts": {
"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",
@ -11,7 +12,6 @@
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"serde": "file:../serde-ts"
},

435
src/de.ts
View file

@ -1,396 +1,135 @@
import { GlobalRegistry, IterResult, Registry } from 'serde'
import { IIterableAccess, MapAccess, IVisitor, IDeserializer, Deserialize, GenericSeed, Visitor } from 'serde/de'
import { unexpected } from './err'
import { getDeserialize, GlobalRegistry, IterResult, Registry } from 'serde'
import { Deserialize, Forwarder, IDeserializer, IterableAccess, IVisitor, MapAccess, Visitor } from 'serde/de'
type Byte = number
const clamp = (value: number, min: number, max: number): number => {
return Math.min(Math.max(value, min), max)
interface Reviver {
<T, U>(key: string, value: T): U
}
const isNumeric = (value: any): value is number => {
return !isNaN(value)
type Entry = [any, any]
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) => {
return value === Token.Period || value === Token.Hyphen || Token.Digit.includes(value)
const parser = (reviver: Reviver) => <T, U>(value: T): U => {
return JSON.parse(value as any, reviver)
}
interface Predicate<T> {
(value: T): boolean
}
export class JSONMapAccess extends MapAccess {
private readonly _entries: Entry[]
private index: number = -1
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 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()) {
constructor(entries: Entry[]) {
super()
this.de = deserializer
this.defaultSeed = new GenericSeed(visitor)
this._entries = entries
}
private seed<V>(): Deserialize<V> {
return this.defaultSeed.deserialize.bind(this.defaultSeed) as Deserialize<V>
static fromObject(value: object) {
return new this(Object.entries(value))
}
private nextItemSeed<T, D extends Deserialize<T>>(seed: D, end: number): IteratorResult<T> {
if (this.de.buffer.peek().next() === end) {
nextKey<T>(seed?: Deserialize<T>): IteratorResult<T> {
this.index += 1
if (this.index >= this._entries.length) {
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) {
const take = this.de.buffer.take()
if (take.next() !== Token.Comma) {
throw unexpected(',', take.toString(), this.de.buffer.position)
}
nextValue<T>(seed?: Deserialize<T>): IteratorResult<T> {
if (this.index >= this._entries.length) {
return IterResult.Done()
} 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 {
private readonly view: Uint8Array
private readonly encoder: TextEncoder
private readonly decoder: TextDecoder
export class JSONIterableAccess extends IterableAccess {
private readonly iterator: Iterator<any>
private index: number = 0
get position() {
return this.index
constructor(iterator: Iterator<any>) {
super()
this.iterator = iterator
}
get length() {
return this.view.byteLength
static fromIterable(iterable: Iterable<any>) {
return new this(iterable[Symbol.iterator]())
}
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): ByteArray {
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
nextElement<T, D extends Deserialize<T>>(seed?: D): IteratorResult<T> {
const result = this.iterator.next()
if (result.done) {
return IterResult.Done()
} else {
const value = seed != null ? seed(result.value) : result.value
return IterResult.Next(value)
}
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 {
readonly buffer: ByteArray
readonly registry: Registry
private readonly input: string
private readonly parser: <T, U>(value: T) => U
constructor(buffer: ByteArray, registry: Registry = GlobalRegistry) {
this.buffer = buffer
this.registry = registry
constructor(input: string, registry: Registry = GlobalRegistry) {
this.input = input
this.parser = parser(deserializer(registry))
}
static fromString(value: string): JSONDeserializer {
return new this(ByteArray.fromString(value))
static fromString(value: string, registry?: Registry) {
return new this(value, registry)
}
deserializeAny<T>(visitor: IVisitor<T>): 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()}"`)
}
deserializeAny<T>(_visitor: IVisitor<T>): T {
return this.parser(this.input)
}
deserializeNull<T, V extends IVisitor<T>>(visitor: V): T {
if (this.buffer.startsWith('null')) {
this.buffer.take(4)
}
deserializeBoolean<T>(visitor: IVisitor<T>): T {
return visitor.visitBoolean(this.parser(this.input))
}
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()
}
deserializeObject<T, V extends IVisitor<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)
}
deserializeObject<T>(visitor: IVisitor<T>): T {
const value = this.parser(this.input) as object
return visitor.visitObject(JSONMapAccess.fromObject(value))
}
deserializeClass<T, V extends IVisitor<T>>(_name: string, visitor: V): T {
return this.deserializeObject(visitor)
deserializeIterable<T>(visitor: IVisitor<T>): T {
const value = this.parser(this.input) as Iterable<any>
return visitor.visitIterable(JSONIterableAccess.fromIterable(value))
}
deserializeString<T, V extends IVisitor<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)
}
deserializeFunction<T>(_visitor: IVisitor<T>): T {
throw new Error('Method not implemented')
}
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 {
const serializer = new JSONSerializer()
serialize(serializer, value)
return serializer.output
return serialize(serializer, value)
}
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> {
private ser: JSONSerializer
private first: boolean = true
interface Stringify {
(value: any): string
}
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()
this.ser = serializer
serializer.write('{')
this.stringify = stringify
}
serializeKey<U>(key: U): void {
if (!this.first) {
this.ser.write(',')
} else {
this.first = false
}
serialize(this.ser, key)
this.ser.write(':')
serializeKey(key: string): void {
this.currentKey = key
}
serializeValue<U>(value: U): void {
serialize(this.ser, value)
this.value[this.currentKey!] = value
this.currentKey = undefined
}
end(): void {
this.ser.write('}')
end(): string {
return this.stringify(this.value)
}
}
class JSONIterableSerializer implements ISerializeIterable<void> {
private ser: JSONSerializer
private first: boolean = true
class JSONSerializeIterable extends SerializeIterable<string> {
private readonly stringify: Stringify
private elements: any[] = []
constructor(serializer: JSONSerializer) {
this.ser = serializer
serializer.write('[')
constructor(stringify: Stringify) {
super()
this.stringify = stringify
}
serializeElement<U>(element: U): void {
if (!this.first) {
this.ser.write(',')
} else {
this.first = false
}
serialize(this.ser, element)
serializeElement<U>(value: U): void {
this.elements.push(value)
}
end(): void {
this.ser.write(']')
end(): string {
return this.stringify(this.elements)
}
}
export class JSONSerializer implements ISerializer<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(): 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 id = <T, U>(_ser: ISerializer<T>, value: U) => value
const serializer = (registry: Registry) => <T>(_key: PropertyKey, value: T) => {
const ser = getSerialize(value, id, registry)
return ser(new IdentitySerializer(), value)
}
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) }
}