import { describe, expect, it } from 'vitest' import { encode } from '../src/index' describe('primitives', () => { it('encodes safe strings without quotes', () => { expect(encode('hello')).toBe('hello') expect(encode('Ada_99')).toBe('Ada_99') }) it('quotes empty string', () => { expect(encode('')).toBe('""') }) it('quotes strings that look like booleans or numbers', () => { expect(encode('true')).toBe('"true"') expect(encode('false')).toBe('"false"') expect(encode('null')).toBe('"null"') expect(encode('42')).toBe('"42"') expect(encode('-3.14')).toBe('"-3.14"') expect(encode('1e-6')).toBe('"1e-6"') expect(encode('05')).toBe('"05"') }) it('escapes control characters in strings', () => { expect(encode('line1\nline2')).toBe('"line1\\nline2"') expect(encode('tab\there')).toBe('"tab\\there"') expect(encode('return\rcarriage')).toBe('"return\\rcarriage"') expect(encode('C:\\Users\\path')).toBe('"C:\\\\Users\\\\path"') }) it('quotes strings with structural characters', () => { expect(encode('[3]: x,y')).toBe('"[3]: x,y"') expect(encode('- item')).toBe('"- item"') expect(encode('[test]')).toBe('"[test]"') expect(encode('{key}')).toBe('"{key}"') }) it('handles Unicode and emoji', () => { expect(encode('café')).toBe('café') expect(encode('你好')).toBe('你好') expect(encode('🚀')).toBe('🚀') expect(encode('hello 👋 world')).toBe('hello 👋 world') }) it('encodes numbers', () => { expect(encode(42)).toBe('42') expect(encode(3.14)).toBe('3.14') expect(encode(-7)).toBe('-7') expect(encode(0)).toBe('0') }) it('handles special numeric values', () => { expect(encode(-0)).toBe('0') expect(encode(1e6)).toBe('1000000') expect(encode(1e-6)).toBe('0.000001') expect(encode(1e20)).toBe('100000000000000000000') expect(encode(Number.MAX_SAFE_INTEGER)).toBe('9007199254740991') }) it('encodes booleans', () => { expect(encode(true)).toBe('true') expect(encode(false)).toBe('false') }) it('encodes null', () => { expect(encode(null)).toBe('null') }) }) describe('objects (simple)', () => { it('preserves key order in objects', () => { const obj = { id: 123, name: 'Ada', active: true, } expect(encode(obj)).toBe('id: 123\nname: Ada\nactive: true') }) it('encodes null values in objects', () => { const obj = { id: 123, value: null } expect(encode(obj)).toBe('id: 123\nvalue: null') }) it('encodes empty objects as empty string', () => { expect(encode({})).toBe('') }) it('quotes string values with special characters', () => { expect(encode({ note: 'a:b' })).toBe('note: "a:b"') expect(encode({ note: 'a,b' })).toBe('note: "a,b"') expect(encode({ text: 'line1\nline2' })).toBe('text: "line1\\nline2"') expect(encode({ text: 'say "hello"' })).toBe('text: "say \\"hello\\""') }) it('quotes string values with leading/trailing spaces', () => { expect(encode({ text: ' padded ' })).toBe('text: " padded "') expect(encode({ text: ' ' })).toBe('text: " "') }) it('quotes string values that look like booleans/numbers', () => { expect(encode({ v: 'true' })).toBe('v: "true"') expect(encode({ v: '42' })).toBe('v: "42"') expect(encode({ v: '-7.5' })).toBe('v: "-7.5"') }) }) describe('objects (keys)', () => { it('quotes keys with special characters', () => { expect(encode({ 'order:id': 7 })).toBe('"order:id": 7') expect(encode({ '[index]': 5 })).toBe('"[index]": 5') expect(encode({ '{key}': 5 })).toBe('"{key}": 5') expect(encode({ 'a,b': 1 })).toBe('"a,b": 1') }) it('quotes keys with spaces or leading hyphens', () => { expect(encode({ 'full name': 'Ada' })).toBe('"full name": Ada') expect(encode({ '-lead': 1 })).toBe('"-lead": 1') expect(encode({ ' a ': 1 })).toBe('" a ": 1') }) it('quotes numeric keys', () => { expect(encode({ 123: 'x' })).toBe('"123": x') }) it('quotes empty string key', () => { expect(encode({ '': 1 })).toBe('"": 1') }) it('escapes control characters in keys', () => { expect(encode({ 'line\nbreak': 1 })).toBe('"line\\nbreak": 1') expect(encode({ 'tab\there': 2 })).toBe('"tab\\there": 2') }) it('escapes quotes in keys', () => { expect(encode({ 'he said "hi"': 1 })).toBe('"he said \\"hi\\"": 1') }) }) describe('nested objects', () => { it('encodes deeply nested objects', () => { const obj = { a: { b: { c: 'deep', }, }, } expect(encode(obj)).toBe('a:\n b:\n c: deep') }) it('encodes empty nested object', () => { expect(encode({ user: {} })).toBe('user:') }) }) describe('arrays of primitives', () => { it('encodes string arrays inline', () => { const obj = { tags: ['admin', 'ops'] } expect(encode(obj)).toBe('tags[2]: admin,ops') }) it('encodes number arrays inline', () => { const obj = { nums: [1, 2, 3] } expect(encode(obj)).toBe('nums[3]: 1,2,3') }) it('encodes mixed primitive arrays inline', () => { const obj = { data: ['x', 'y', true, 10] } expect(encode(obj)).toBe('data[4]: x,y,true,10') }) it('encodes empty arrays', () => { const obj = { items: [] } expect(encode(obj)).toBe('items[0]:') }) it('handles empty string in arrays', () => { const obj = { items: [''] } expect(encode(obj)).toBe('items[1]: ""') const obj2 = { items: ['a', '', 'b'] } expect(encode(obj2)).toBe('items[3]: a,"",b') }) it('handles whitespace-only strings in arrays', () => { const obj = { items: [' ', ' '] } expect(encode(obj)).toBe('items[2]: " "," "') }) it('quotes array strings with special characters', () => { const obj = { items: ['a', 'b,c', 'd:e'] } expect(encode(obj)).toBe('items[3]: a,"b,c","d:e"') }) it('quotes strings that look like booleans/numbers in arrays', () => { const obj = { items: ['x', 'true', '42', '-3.14'] } expect(encode(obj)).toBe('items[4]: x,"true","42","-3.14"') }) it('quotes strings with structural meanings in arrays', () => { const obj = { items: ['[5]', '- item', '{key}'] } expect(encode(obj)).toBe('items[3]: "[5]","- item","{key}"') }) }) describe('arrays of objects (tabular and list items)', () => { it('encodes arrays of similar objects in tabular format', () => { const obj = { items: [ { sku: 'A1', qty: 2, price: 9.99 }, { sku: 'B2', qty: 1, price: 14.5 }, ], } expect(encode(obj)).toBe('items[2]{sku,qty,price}:\n A1,2,9.99\n B2,1,14.5') }) it('handles null values in tabular format', () => { const obj = { items: [ { id: 1, value: null }, { id: 2, value: 'test' }, ], } expect(encode(obj)).toBe('items[2]{id,value}:\n 1,null\n 2,test') }) it('quotes strings in tabular rows when needed', () => { const obj = { items: [ { sku: 'A,1', desc: 'cool', qty: 2 }, { sku: 'B2', desc: 'wip: test', qty: 1 }, ], } expect(encode(obj)).toBe('items[2]{sku,desc,qty}:\n "A,1",cool,2\n B2,"wip: test",1') }) it('quotes ambiguous strings in tabular rows', () => { const obj = { items: [ { id: 1, status: 'true' }, { id: 2, status: 'false' }, ], } expect(encode(obj)).toBe('items[2]{id,status}:\n 1,"true"\n 2,"false"') }) it('handles tabular arrays with keys needing quotes', () => { const obj = { items: [ { 'order:id': 1, 'full name': 'Ada' }, { 'order:id': 2, 'full name': 'Bob' }, ], } expect(encode(obj)).toBe('items[2]{"order:id","full name"}:\n 1,Ada\n 2,Bob') }) it('uses list format for objects with different fields', () => { const obj = { items: [ { id: 1, name: 'First' }, { id: 2, name: 'Second', extra: true }, ], } expect(encode(obj)).toBe( 'items[2]:\n' + ' - id: 1\n' + ' name: First\n' + ' - id: 2\n' + ' name: Second\n' + ' extra: true', ) }) it('uses list format for objects with nested values', () => { const obj = { items: [ { id: 1, nested: { x: 1 } }, ], } expect(encode(obj)).toBe( 'items[1]:\n' + ' - id: 1\n' + ' nested:\n' + ' x: 1', ) }) it('uses field order from first object for tabular headers', () => { const obj = { items: [ { a: 1, b: 2, c: 3 }, { c: 30, b: 20, a: 10 }, ], } expect(encode(obj)).toBe('items[2]{a,b,c}:\n 1,2,3\n 10,20,30') }) it('uses list format for one object with nested column', () => { const obj = { items: [ { id: 1, data: 'string' }, { id: 2, data: { nested: true } }, ], } expect(encode(obj)).toBe( 'items[2]:\n' + ' - id: 1\n' + ' data: string\n' + ' - id: 2\n' + ' data:\n' + ' nested: true', ) }) }) describe('arrays of arrays (primitives only)', () => { it('encodes nested arrays of primitives', () => { const obj = { pairs: [['a', 'b'], ['c', 'd']], } expect(encode(obj)).toBe('pairs[2]:\n - [2]: a,b\n - [2]: c,d') }) it('quotes nested array strings when needed', () => { const obj = { pairs: [['a', 'b'], ['c,d', 'e:f', 'true']], } expect(encode(obj)).toBe('pairs[2]:\n - [2]: a,b\n - [3]: "c,d","e:f","true"') }) it('handles empty inner arrays', () => { const obj = { pairs: [[], []], } expect(encode(obj)).toBe('pairs[2]:\n - [0]:\n - [0]:') }) it('handles mixed-length inner arrays', () => { const obj = { pairs: [[1], [2, 3]], } expect(encode(obj)).toBe('pairs[2]:\n - [1]: 1\n - [2]: 2,3') }) }) describe('root arrays', () => { it('encodes arrays of primitives at root level', () => { const arr = ['x', 'y', 'true', true, 10] expect(encode(arr)).toBe('[5]: x,y,"true",true,10') }) it('encodes arrays of similar objects in tabular format', () => { const arr = [{ id: 1 }, { id: 2 }] expect(encode(arr)).toBe('[2]{id}:\n 1\n 2') }) it('encodes arrays of different objects in list format', () => { const arr = [{ id: 1 }, { id: 2, name: 'Ada' }] expect(encode(arr)).toBe('[2]:\n - id: 1\n - id: 2\n name: Ada') }) it('encodes empty arrays at root level', () => { expect(encode([])).toBe('[0]:') }) it('encodes root arrays of arrays', () => { const arr = [[1, 2], []] expect(encode(arr)).toBe('[2]:\n - [2]: 1,2\n - [0]:') }) }) describe('complex structures', () => { it('encodes objects with mixed arrays and nested objects', () => { const obj = { user: { id: 123, name: 'Ada', tags: ['admin', 'ops'], active: true, prefs: [], }, } expect(encode(obj)).toBe( 'user:\n' + ' id: 123\n' + ' name: Ada\n' + ' tags[2]: admin,ops\n' + ' active: true\n' + ' prefs[0]:', ) }) }) describe('mixed arrays', () => { it('uses list format for arrays mixing primitives and objects', () => { const obj = { items: [1, { a: 1 }, 'text'], } expect(encode(obj)).toBe( 'items[3]:\n' + ' - 1\n' + ' - a: 1\n' + ' - text', ) }) it('uses list format for arrays mixing objects and arrays', () => { const obj = { items: [{ a: 1 }, [1, 2]], } expect(encode(obj)).toBe( 'items[2]:\n' + ' - a: 1\n' + ' - [2]: 1,2', ) }) }) describe('whitespace and formatting invariants', () => { it('produces no trailing spaces at end of lines', () => { const obj = { user: { id: 123, name: 'Ada', }, items: ['a', 'b'], } const result = encode(obj) const lines = result.split('\n') for (const line of lines) { expect(line).not.toMatch(/ $/) } }) it('produces no trailing newline at end of output', () => { const obj = { id: 123 } const result = encode(obj) expect(result).not.toMatch(/\n$/) }) }) describe('non-JSON-serializable values', () => { it('converts BigInt to string', () => { expect(encode(BigInt(123))).toBe('123') expect(encode({ id: BigInt(456) })).toBe('id: 456') }) it('converts Date to ISO string', () => { const date = new Date('2025-01-01T00:00:00.000Z') expect(encode(date)).toBe('"2025-01-01T00:00:00.000Z"') expect(encode({ created: date })).toBe('created: "2025-01-01T00:00:00.000Z"') }) it('converts undefined to null', () => { expect(encode(undefined)).toBe('null') expect(encode({ value: undefined })).toBe('value: null') }) it('converts non-finite numbers to null', () => { expect(encode(Infinity)).toBe('null') expect(encode(-Infinity)).toBe('null') expect(encode(Number.NaN)).toBe('null') }) it('converts functions to null', () => { expect(encode(() => {})).toBe('null') expect(encode({ fn: () => {} })).toBe('fn: null') }) it('converts symbols to null', () => { expect(encode(Symbol('test'))).toBe('null') expect(encode({ sym: Symbol('test') })).toBe('sym: null') }) })