mirror of
https://github.com/voson-wang/toon.git
synced 2026-01-29 15:24:10 +08:00
feat(cli): stream output for both encoding and decoding
This commit is contained in:
@@ -153,15 +153,18 @@ describe('toon CLI', () => {
|
||||
|
||||
const cleanup = mockStdin(toonInput)
|
||||
|
||||
const stdout: string[] = []
|
||||
vi.spyOn(console, 'log').mockImplementation((message?: unknown) => {
|
||||
stdout.push(String(message ?? ''))
|
||||
const writeChunks: string[] = []
|
||||
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
||||
writeChunks.push(String(chunk))
|
||||
return true
|
||||
})
|
||||
|
||||
try {
|
||||
await runCli({ rawArgs: ['--decode'] })
|
||||
expect(stdout).toHaveLength(1)
|
||||
const result = JSON.parse(stdout?.at(0) ?? '')
|
||||
const fullOutput = writeChunks.join('')
|
||||
// Remove trailing newline before parsing
|
||||
const jsonOutput = fullOutput.endsWith('\n') ? fullOutput.slice(0, -1) : fullOutput
|
||||
const result = JSON.parse(jsonOutput)
|
||||
expect(result).toEqual(data)
|
||||
}
|
||||
finally {
|
||||
@@ -279,16 +282,19 @@ describe('toon CLI', () => {
|
||||
const toonInput = encode(data)
|
||||
const cleanup = mockStdin(toonInput)
|
||||
|
||||
const stdout: string[] = []
|
||||
vi.spyOn(console, 'log').mockImplementation((message?: unknown) => {
|
||||
stdout.push(String(message ?? ''))
|
||||
const writeChunks: string[] = []
|
||||
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
||||
writeChunks.push(String(chunk))
|
||||
return true
|
||||
})
|
||||
|
||||
try {
|
||||
await runCli({ rawArgs: ['--decode', '--no-strict'] })
|
||||
|
||||
expect(stdout).toHaveLength(1)
|
||||
const result = JSON.parse(stdout?.at(0) ?? '')
|
||||
const fullOutput = writeChunks.join('')
|
||||
// Remove trailing newline before parsing
|
||||
const jsonOutput = fullOutput.endsWith('\n') ? fullOutput.slice(0, -1) : fullOutput
|
||||
const result = JSON.parse(jsonOutput)
|
||||
expect(result).toEqual(data)
|
||||
}
|
||||
finally {
|
||||
|
||||
245
packages/cli/test/json-stringify-stream.test.ts
Normal file
245
packages/cli/test/json-stringify-stream.test.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { jsonStringifyLines } from '../src/json-stringify-stream'
|
||||
|
||||
describe('jsonStringifyLines', () => {
|
||||
describe('primitives', () => {
|
||||
it('stringifies null', () => {
|
||||
expect(join(jsonStringifyLines(null, 0))).toBe(JSON.stringify(null))
|
||||
expect(join(jsonStringifyLines(null, 2))).toBe(JSON.stringify(null, null, 2))
|
||||
})
|
||||
|
||||
it('stringifies booleans', () => {
|
||||
expect(join(jsonStringifyLines(true, 0))).toBe(JSON.stringify(true))
|
||||
expect(join(jsonStringifyLines(false, 0))).toBe(JSON.stringify(false))
|
||||
expect(join(jsonStringifyLines(true, 2))).toBe(JSON.stringify(true, null, 2))
|
||||
})
|
||||
|
||||
it('stringifies numbers', () => {
|
||||
expect(join(jsonStringifyLines(0, 0))).toBe(JSON.stringify(0))
|
||||
expect(join(jsonStringifyLines(42, 0))).toBe(JSON.stringify(42))
|
||||
expect(join(jsonStringifyLines(-17, 0))).toBe(JSON.stringify(-17))
|
||||
expect(join(jsonStringifyLines(3.14159, 0))).toBe(JSON.stringify(3.14159))
|
||||
expect(join(jsonStringifyLines(1e10, 2))).toBe(JSON.stringify(1e10, null, 2))
|
||||
})
|
||||
|
||||
it('stringifies strings', () => {
|
||||
expect(join(jsonStringifyLines('', 0))).toBe(JSON.stringify(''))
|
||||
expect(join(jsonStringifyLines('hello', 0))).toBe(JSON.stringify('hello'))
|
||||
expect(join(jsonStringifyLines('with "quotes"', 0))).toBe(JSON.stringify('with "quotes"'))
|
||||
expect(join(jsonStringifyLines('with\nnewlines', 2))).toBe(JSON.stringify('with\nnewlines', null, 2))
|
||||
expect(join(jsonStringifyLines('with\ttabs', 0))).toBe(JSON.stringify('with\ttabs'))
|
||||
})
|
||||
|
||||
it('converts undefined to null', () => {
|
||||
expect(join(jsonStringifyLines(undefined, 0))).toBe('null')
|
||||
expect(join(jsonStringifyLines(undefined, 2))).toBe('null')
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty containers', () => {
|
||||
it('stringifies empty arrays', () => {
|
||||
expect(join(jsonStringifyLines([], 0))).toBe(JSON.stringify([], null, 0))
|
||||
expect(join(jsonStringifyLines([], 2))).toBe(JSON.stringify([], null, 2))
|
||||
})
|
||||
|
||||
it('stringifies empty objects', () => {
|
||||
expect(join(jsonStringifyLines({}, 0))).toBe(JSON.stringify({}, null, 0))
|
||||
expect(join(jsonStringifyLines({}, 2))).toBe(JSON.stringify({}, null, 2))
|
||||
})
|
||||
})
|
||||
|
||||
describe('arrays', () => {
|
||||
it('stringifies arrays with compact formatting (indent=0)', () => {
|
||||
const value = [1, 2, 3]
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
})
|
||||
|
||||
it('stringifies arrays with pretty formatting (indent=2)', () => {
|
||||
const value = [1, 2, 3]
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('stringifies mixed-type arrays', () => {
|
||||
const value = [1, 'two', true, null, { key: 'value' }]
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('stringifies nested arrays', () => {
|
||||
const value = [[1, 2], [3, 4], [5, 6]]
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('stringifies deeply nested arrays', () => {
|
||||
const value = [[[1]], [[2]], [[3]]]
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
expect(join(jsonStringifyLines(value, 4))).toBe(JSON.stringify(value, null, 4))
|
||||
})
|
||||
})
|
||||
|
||||
describe('objects', () => {
|
||||
it('stringifies simple objects with compact formatting', () => {
|
||||
const value = { a: 1, b: 2, c: 3 }
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
})
|
||||
|
||||
it('stringifies simple objects with pretty formatting', () => {
|
||||
const value = { a: 1, b: 2, c: 3 }
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('stringifies objects with mixed value types', () => {
|
||||
const value = {
|
||||
num: 42,
|
||||
str: 'hello',
|
||||
bool: true,
|
||||
nil: null,
|
||||
arr: [1, 2, 3],
|
||||
}
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('stringifies nested objects', () => {
|
||||
const value = {
|
||||
level1: {
|
||||
level2: {
|
||||
level3: 'deep',
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('preserves key order', () => {
|
||||
const value = { z: 1, a: 2, m: 3 }
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('handles special characters in keys', () => {
|
||||
const value = {
|
||||
'normal-key': 1,
|
||||
'key with spaces': 2,
|
||||
'key:with:colons': 3,
|
||||
'key"with"quotes': 4,
|
||||
}
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
})
|
||||
|
||||
describe('complex nested structures', () => {
|
||||
it('stringifies objects containing arrays', () => {
|
||||
const value = {
|
||||
name: 'Alice',
|
||||
scores: [95, 87, 92],
|
||||
metadata: {
|
||||
tags: ['math', 'science'],
|
||||
},
|
||||
}
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('stringifies arrays of objects', () => {
|
||||
const value = [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
{ id: 3, name: 'Charlie' },
|
||||
]
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('stringifies deeply nested mixed structures', () => {
|
||||
const value = {
|
||||
users: [
|
||||
{
|
||||
name: 'Alice',
|
||||
roles: ['admin', 'user'],
|
||||
settings: {
|
||||
theme: 'dark',
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Bob',
|
||||
roles: ['user'],
|
||||
settings: {
|
||||
theme: 'light',
|
||||
notifications: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
count: 2,
|
||||
}
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
})
|
||||
|
||||
describe('indentation levels', () => {
|
||||
const value = { a: [1, 2], b: { c: 3 } }
|
||||
|
||||
it('handles indent=0 (compact)', () => {
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
})
|
||||
|
||||
it('handles indent=2', () => {
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('handles indent=4', () => {
|
||||
expect(join(jsonStringifyLines(value, 4))).toBe(JSON.stringify(value, null, 4))
|
||||
})
|
||||
|
||||
it('handles indent=8', () => {
|
||||
expect(join(jsonStringifyLines(value, 8))).toBe(JSON.stringify(value, null, 8))
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles arrays with undefined values (converted to null)', () => {
|
||||
const value = [1, undefined, 3]
|
||||
const expected = JSON.stringify(value, null, 2)
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(expected)
|
||||
})
|
||||
|
||||
it('handles single-element arrays', () => {
|
||||
const value = [42]
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('handles single-property objects', () => {
|
||||
const value = { only: 'one' }
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('handles objects with many properties', () => {
|
||||
const value: Record<string, number> = {}
|
||||
for (let i = 0; i < 100; i++) {
|
||||
value[`key${i}`] = i
|
||||
}
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
it('handles large arrays', () => {
|
||||
const value = Array.from({ length: 1000 }, (_, i) => i)
|
||||
expect(join(jsonStringifyLines(value, 0))).toBe(JSON.stringify(value, null, 0))
|
||||
expect(join(jsonStringifyLines(value, 2))).toBe(JSON.stringify(value, null, 2))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Joins chunks from an iterable into a single string.
|
||||
*/
|
||||
function join(iter: Iterable<string>): string {
|
||||
return Array.from(iter).join('')
|
||||
}
|
||||
Reference in New Issue
Block a user