toon-format--toon
408 行
11 KiB
TypeScript
408 行
11 KiB
TypeScript
import type { EncodeReplacer, JsonObject, JsonValue } from '../src/types'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { decode, encode } from '../src/index'
|
|
|
|
describe('replacer function', () => {
|
|
describe('basic filtering', () => {
|
|
it('removes properties by returning undefined', () => {
|
|
const input = { name: 'Alice', password: 'secret', email: 'alice@example.com' }
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
if (key === 'password')
|
|
return undefined
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual({ name: 'Alice', email: 'alice@example.com' })
|
|
expect(decoded).not.toHaveProperty('password')
|
|
})
|
|
|
|
it('removes array elements by returning undefined', () => {
|
|
const input = [1, 2, 3, 4, 5]
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
if (typeof value === 'number' && value % 2 === 0)
|
|
return undefined
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual([1, 3, 5])
|
|
})
|
|
|
|
it('handles deeply nested filtering', () => {
|
|
const input = {
|
|
users: [
|
|
{ name: 'Alice', password: 'secret1', role: 'admin' },
|
|
{ name: 'Bob', password: 'secret2', role: 'user' },
|
|
],
|
|
}
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
if (key === 'password')
|
|
return undefined
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual({
|
|
users: [
|
|
{ name: 'Alice', role: 'admin' },
|
|
{ name: 'Bob', role: 'user' },
|
|
],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('value transformation', () => {
|
|
it('transforms primitive values', () => {
|
|
const input = { name: 'alice', age: 30 }
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
if (typeof value === 'string')
|
|
return value.toUpperCase()
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual({ name: 'ALICE', age: 30 })
|
|
})
|
|
|
|
it('transforms objects', () => {
|
|
const input = { user: { name: 'Alice' } }
|
|
const replacer: EncodeReplacer = (key, value, path) => {
|
|
if (path.length === 1 && typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
return { ...value as object, _id: `${key}_123` }
|
|
}
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual({
|
|
user: { name: 'Alice', _id: 'user_123' },
|
|
})
|
|
})
|
|
|
|
it('transforms arrays', () => {
|
|
const input = { numbers: [1, 2, 3] }
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
if (typeof value === 'number')
|
|
return value * 2
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual({ numbers: [2, 4, 6] })
|
|
})
|
|
})
|
|
|
|
describe('root value handling', () => {
|
|
it('calls replacer on root value with empty string key', () => {
|
|
const input = { value: 42 }
|
|
let rootKeySeen = false
|
|
let rootPathSeen = false
|
|
|
|
const replacer: EncodeReplacer = (key, value, path) => {
|
|
if (key === '' && path.length === 0) {
|
|
rootKeySeen = true
|
|
rootPathSeen = true
|
|
}
|
|
return value
|
|
}
|
|
|
|
encode(input, { replacer })
|
|
|
|
expect(rootKeySeen).toBe(true)
|
|
expect(rootPathSeen).toBe(true)
|
|
})
|
|
|
|
it('transforms root object', () => {
|
|
const input = { name: 'Alice' }
|
|
const replacer: EncodeReplacer = (key, value, path) => {
|
|
if (path.length === 0) {
|
|
return { ...value as object, timestamp: 1234567890 }
|
|
}
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual({ name: 'Alice', timestamp: 1234567890 })
|
|
})
|
|
|
|
it('does not omit root when replacer returns undefined', () => {
|
|
const input = { name: 'Alice' }
|
|
const replacer: EncodeReplacer = (key, value, path) => {
|
|
if (path.length === 0)
|
|
return undefined
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual({ name: 'Alice' })
|
|
})
|
|
|
|
it('handles primitive root values', () => {
|
|
const input = 'hello'
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
if (typeof value === 'string')
|
|
return value.toUpperCase()
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
expect(result).toBe('HELLO')
|
|
})
|
|
|
|
it('provides correct arguments to root call', () => {
|
|
const input = { data: 'test' }
|
|
const calls: { key: string, path: (string | number)[] }[] = []
|
|
|
|
const replacer: EncodeReplacer = (key, value, path) => {
|
|
calls.push({ key, path: [...path] })
|
|
return value
|
|
}
|
|
|
|
encode(input, { replacer })
|
|
|
|
expect(calls[0]).toEqual({ key: '', path: [] })
|
|
})
|
|
})
|
|
|
|
describe('path tracking', () => {
|
|
it('provides correct paths for nested objects', () => {
|
|
const input = {
|
|
user: {
|
|
profile: {
|
|
name: 'Alice',
|
|
},
|
|
},
|
|
}
|
|
const paths: string[] = []
|
|
|
|
const replacer: EncodeReplacer = (key, value, path) => {
|
|
paths.push(path.join('.'))
|
|
return value
|
|
}
|
|
|
|
encode(input, { replacer })
|
|
|
|
expect(paths).toContain('') // root
|
|
expect(paths).toContain('user')
|
|
expect(paths).toContain('user.profile')
|
|
expect(paths).toContain('user.profile.name')
|
|
})
|
|
|
|
it('provides correct paths for arrays with string indices', () => {
|
|
const input = { items: ['a', 'b', 'c'] }
|
|
const seenKeys: string[] = []
|
|
|
|
const replacer: EncodeReplacer = (key, value, path) => {
|
|
if (path.length > 0 && path[path.length - 1] !== 'items') {
|
|
seenKeys.push(key)
|
|
}
|
|
return value
|
|
}
|
|
|
|
encode(input, { replacer })
|
|
|
|
expect(seenKeys).toEqual(['0', '1', '2'])
|
|
})
|
|
|
|
it('provides correct paths for nested arrays', () => {
|
|
const input = {
|
|
matrix: [
|
|
[1, 2],
|
|
[3, 4],
|
|
],
|
|
}
|
|
const paths: string[] = []
|
|
|
|
const replacer: EncodeReplacer = (key, value, path) => {
|
|
if (typeof value === 'number') {
|
|
paths.push(`${path.join('.')} (key="${key}")`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
encode(input, { replacer })
|
|
|
|
expect(paths).toContain('matrix.0.0 (key="0")')
|
|
expect(paths).toContain('matrix.0.1 (key="1")')
|
|
expect(paths).toContain('matrix.1.0 (key="0")')
|
|
expect(paths).toContain('matrix.1.1 (key="1")')
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('handles empty objects', () => {
|
|
const input = {}
|
|
const replacer: EncodeReplacer = (key, value) => value
|
|
|
|
const result = encode(input, { replacer })
|
|
expect(result).toBe('')
|
|
})
|
|
|
|
it('handles empty arrays', () => {
|
|
const input: JsonValue[] = []
|
|
const replacer: EncodeReplacer = (key, value) => value
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
expect(decoded).toEqual([])
|
|
})
|
|
|
|
it('handles null values', () => {
|
|
const input = { value: null }
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
if (value === null)
|
|
return 'NULL'
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
expect(decoded).toEqual({ value: 'NULL' })
|
|
})
|
|
|
|
it('re-normalizes non-JsonValue returns', () => {
|
|
const input = { date: '2025-01-01' }
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
// Return a Date object (will be normalized to ISO string)
|
|
if (key === 'date')
|
|
return new Date(value as string)
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result) as JsonObject
|
|
|
|
expect(typeof decoded.date).toBe('string')
|
|
expect(decoded.date).toMatch(/^\d{4}-\d{2}-\d{2}T/)
|
|
})
|
|
|
|
it('handles all properties being filtered out', () => {
|
|
const input = { a: 1, b: 2, c: 3 }
|
|
const replacer: EncodeReplacer = (key, value, path) => {
|
|
if (path.length > 0)
|
|
return undefined
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual({})
|
|
})
|
|
|
|
it('handles all array elements being filtered out', () => {
|
|
const input = [1, 2, 3]
|
|
const replacer: EncodeReplacer = (key, value, path) => {
|
|
if (path.length > 0)
|
|
return undefined
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual([])
|
|
})
|
|
|
|
it('handles nested objects with mixed omissions', () => {
|
|
const input = {
|
|
keep: 'this',
|
|
remove: 'that',
|
|
nested: {
|
|
keep: 'nested keep',
|
|
remove: 'nested remove',
|
|
},
|
|
}
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
if (key === 'remove')
|
|
return undefined
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual({
|
|
keep: 'this',
|
|
nested: {
|
|
keep: 'nested keep',
|
|
},
|
|
})
|
|
})
|
|
|
|
it('handles arrays with some elements removed', () => {
|
|
const input = { items: [{ id: 1, keep: true }, { id: 2, keep: false }, { id: 3, keep: true }] }
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
if (typeof value === 'object' && value !== null && !Array.isArray(value) && 'keep' in value && value.keep === false) {
|
|
return undefined
|
|
}
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer })
|
|
const decoded = decode(result)
|
|
|
|
expect(decoded).toEqual({
|
|
items: [{ id: 1, keep: true }, { id: 3, keep: true }],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('integration with other options', () => {
|
|
it('works with keyFolding', () => {
|
|
const input = {
|
|
user: {
|
|
profile: {
|
|
name: 'Alice',
|
|
},
|
|
},
|
|
}
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
if (typeof value === 'string')
|
|
return value.toUpperCase()
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer, keyFolding: 'safe' })
|
|
expect(result).toContain('user.profile.name: ALICE')
|
|
})
|
|
|
|
it('works with custom delimiters', () => {
|
|
const input = { items: [1, 2, 3] }
|
|
const replacer: EncodeReplacer = (key, value) => {
|
|
if (typeof value === 'number')
|
|
return value * 10
|
|
return value
|
|
}
|
|
|
|
const result = encode(input, { replacer, delimiter: '\t' })
|
|
expect(result).toContain('10\t20\t30')
|
|
})
|
|
|
|
it('works with custom indent', () => {
|
|
const input = { user: { name: 'Alice' } }
|
|
const replacer: EncodeReplacer = (key, value) => value
|
|
|
|
const result = encode(input, { replacer, indent: 4 })
|
|
expect(result).toContain(' name: Alice')
|
|
})
|
|
})
|
|
})
|