UNPKG

ts-data-forge

Version:

[![npm version](https://img.shields.io/npm/v/ts-data-forge.svg)](https://www.npmjs.com/package/ts-data-forge) [![npm downloads](https://img.shields.io/npm/dm/ts-data-forge.svg)](https://www.npmjs.com/package/ts-data-forge) [![License](https://img.shields.

748 lines (535 loc) 19.5 kB
import { Arr } from '../array/index.mjs'; import { Result } from '../functional/index.mjs'; import { hasKey, isRecord, isString } from '../guard/index.mjs'; import { Json } from './json.mjs'; describe('parse', () => { test('should parse primitive values', () => { assert.deepStrictEqual(Json.parse('"hello"'), Result.ok('hello')); assert.deepStrictEqual(Json.parse('42'), Result.ok(42)); assert.deepStrictEqual(Json.parse('true'), Result.ok(true)); assert.deepStrictEqual(Json.parse('false'), Result.ok(false)); assert.deepStrictEqual(Json.parse('null'), Result.ok(null)); }); test('should parse arrays', () => { assert.deepStrictEqual(Json.parse('[1,2,3]'), Result.ok([1, 2, 3])); assert.deepStrictEqual( Json.parse('["a","b","c"]'), Result.ok(['a', 'b', 'c']), ); assert.deepStrictEqual( Json.parse('[1,"two",true,null]'), Result.ok([1, 'two', true, null]), ); }); test('should parse objects', () => { assert.deepStrictEqual( Json.parse('{"a":1,"b":2}'), Result.ok({ a: 1, b: 2 }), ); assert.deepStrictEqual( Json.parse('{"name":"test","value":42}'), Result.ok({ name: 'test', value: 42, }), ); }); test('should parse nested structures', () => { const json = '{"level1":{"level2":{"array":[1,2,{"level3":"deep"}]}}}'; const expected = { level1: { level2: { array: [1, 2, { level3: 'deep' }], }, }, }; assert.deepStrictEqual(Json.parse(json), Result.ok(expected)); }); test('should handle whitespace', () => { assert.deepStrictEqual( Json.parse(' { "a" : 1 , "b" : 2 } '), Result.ok({ a: 1, b: 2 }), ); assert.deepStrictEqual( Json.parse('\n[\n 1,\n 2,\n 3\n]\n'), Result.ok([1, 2, 3]), ); }); test('should return error for invalid JSON', () => { assert.isTrue(Result.isErr(Json.parse('invalid'))); assert.isTrue(Result.isErr(Json.parse('{missing quotes: true}'))); assert.isTrue(Result.isErr(Json.parse('[1,2,]'))); // Trailing comma assert.isTrue(Result.isErr(Json.parse('undefined'))); }); test('should return parsed value for valid JSON', () => { assert.deepStrictEqual(Json.parse('{"a":1}'), Result.ok({ a: 1 })); assert.deepStrictEqual(Json.parse('[1,2,3]'), Result.ok([1, 2, 3])); assert.deepStrictEqual(Json.parse('"string"'), Result.ok('string')); assert.deepStrictEqual(Json.parse('42'), Result.ok(42)); assert.deepStrictEqual(Json.parse('true'), Result.ok(true)); assert.deepStrictEqual(Json.parse('null'), Result.ok(null)); }); test('should return error for invalid JSON cases', () => { assert.isTrue(Result.isErr(Json.parse('invalid'))); assert.isTrue(Result.isErr(Json.parse('{bad json}'))); assert.isTrue(Result.isErr(Json.parse('[1,2,]'))); assert.isTrue(Result.isErr(Json.parse('undefined'))); assert.isTrue(Result.isErr(Json.parse(''))); }); test('should handle edge cases', () => { assert.deepStrictEqual(Json.parse('0'), Result.ok(0)); assert.deepStrictEqual(Json.parse('""'), Result.ok('')); assert.deepStrictEqual(Json.parse('[]'), Result.ok([])); assert.deepStrictEqual(Json.parse('{}'), Result.ok({})); }); test('should not throw errors', () => { expect(() => Json.parse('{{{')).not.toThrow(); expect(() => Json.parse('null null')).not.toThrow(); expect(() => Json.parse(String(undefined))).not.toThrow(); }); test('should use reviver function to transform values', () => { const dateReviver = (_key: string, value: unknown): unknown => { if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/u.test(value)) { return new Date(value); } return value; }; const jsonString = '{"name":"test","created":"2023-12-01T10:00:00.000Z"}'; const result = Json.parse(jsonString, dateReviver); assert.isTrue(Result.isOk(result)); if ( isRecord(result.value) && hasKey(result.value, 'name') && hasKey(result.value, 'created') ) { expect(result.value.name).toBe('test'); expect(result.value.created).toBeInstanceOf(Date); } }); test('should handle reviver returning different types', () => { const transformReviver = (key: string, value: unknown): unknown => { if (key === 'number' && typeof value === 'string') { return Number.parseInt(value, 10); } return value; }; const result = Json.parse( '{"number":"42","text":"hello"}', transformReviver, ); assert.isTrue(Result.isOk(result)); expect(result.value).toHaveProperty('number'); expect(result.value).toHaveProperty('text'); if ( isRecord(result.value) && hasKey(result.value, 'number') && hasKey(result.value, 'text') ) { expect(result.value.number).toBe(42); expect(result.value.text).toBe('hello'); } }); }); describe('stringify', () => { test('should stringify primitive values', () => { assert.deepStrictEqual(Json.stringify('hello'), Result.ok('"hello"')); assert.deepStrictEqual(Json.stringify(42), Result.ok('42')); assert.deepStrictEqual(Json.stringify(true), Result.ok('true')); assert.deepStrictEqual(Json.stringify(null), Result.ok('null')); }); test('should stringify arrays', () => { assert.deepStrictEqual(Json.stringify([1, 2, 3]), Result.ok('[1,2,3]')); assert.deepStrictEqual( Json.stringify(['a', 'b', 'c']), Result.ok('["a","b","c"]'), ); assert.deepStrictEqual( Json.stringify([1, 'two', true, null]), Result.ok('[1,"two",true,null]'), ); }); test('should stringify objects', () => { assert.deepStrictEqual( Json.stringify({ a: 1, b: 2 }), Result.ok('{"a":1,"b":2}'), ); assert.deepStrictEqual( Json.stringify({ name: 'test', value: 42 }), Result.ok('{"name":"test","value":42}'), ); }); test('should stringify nested structures', () => { const nested = { level1: { level2: { array: [1, 2, { level3: 'deep' }], }, }, }; assert.deepStrictEqual( Json.stringify(nested), Result.ok('{"level1":{"level2":{"array":[1,2,{"level3":"deep"}]}}}'), ); }); test('should handle empty structures', () => { assert.deepStrictEqual(Json.stringify({}), Result.ok('{}')); assert.deepStrictEqual(Json.stringify([]), Result.ok('[]')); }); test('should handle special string values', () => { assert.deepStrictEqual( Json.stringify('with "quotes"'), Result.ok(String.raw`"with \"quotes\""`), ); assert.deepStrictEqual( Json.stringify('with\nnewline'), Result.ok(String.raw`"with\nnewline"`), ); assert.deepStrictEqual( Json.stringify('with\ttab'), Result.ok(String.raw`"with\ttab"`), ); }); test('should return stringified value for valid JSON values', () => { assert.deepStrictEqual(Json.stringify({ a: 1 }), Result.ok('{"a":1}')); assert.deepStrictEqual(Json.stringify([1, 2, 3]), Result.ok('[1,2,3]')); assert.deepStrictEqual(Json.stringify('string'), Result.ok('"string"')); assert.deepStrictEqual(Json.stringify(42), Result.ok('42')); assert.deepStrictEqual(Json.stringify(true), Result.ok('true')); assert.deepStrictEqual(Json.stringify(null), Result.ok('null')); }); test('should handle non-serializable values', () => { assert.deepStrictEqual(Json.stringify(undefined), Result.ok(undefined)); assert.deepStrictEqual( Json.stringify(Symbol('test')), Result.ok(undefined), ); assert.deepStrictEqual( Json.stringify(() => {}), Result.ok(undefined), ); // BigInt should cause an error assert.isTrue(Result.isErr(Json.stringify(123n))); }); test('should handle circular references', () => { const mut_obj: any = { a: 1 }; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access mut_obj.circular = mut_obj; assert.isTrue(Result.isErr(Json.stringify(mut_obj))); }); test('should handle objects with toJSON method', () => { const obj = { toJSON: () => ({ custom: 'value' }), }; assert.deepStrictEqual( Json.stringify(obj), Result.ok('{"custom":"value"}'), ); }); test('should handle Date objects', () => { const date = new Date('2023-01-01T00:00:00.000Z'); assert.deepStrictEqual( Json.stringify(date), Result.ok('"2023-01-01T00:00:00.000Z"'), ); }); test('should not throw errors', () => { const mut_circularArray: any[] = []; mut_circularArray.push(mut_circularArray); expect(() => Json.stringify(mut_circularArray)).not.toThrow(); expect(() => Json.stringify({ fn: () => {} })).not.toThrow(); }); test('should use replacer function to filter values', () => { const data = { name: 'John', password: 'secret123', email: 'john@example.com', }; const secureReplacer = (key: string, value: unknown): unknown => { if (key === 'password') return '[REDACTED]'; return value; }; const result = Json.stringify(data, secureReplacer); assert.isTrue(Result.isOk(result)); expect(result.value).toContain('[REDACTED]'); expect(result.value).not.toContain('secret123'); }); test('should format output with space parameter (number)', () => { const data = { a: 1, b: 2 }; const result = Json.stringify(data, undefined, 2); assert.isTrue(Result.isOk(result)); expect(result.value).toContain('\n'); expect(result.value).toContain(' '); // 2 spaces indentation }); test('should format output with space parameter (string)', () => { const data = { a: 1, b: 2 }; const result = Json.stringify(data, undefined, '\t'); assert.isTrue(Result.isOk(result)); expect(result.value).toContain('\n'); expect(result.value).toContain('\t'); // tab indentation }); }); describe('stringifySelected', () => { test('should include only selected properties', () => { const user = { id: 1, name: 'Alice', email: 'alice@example.com', password: 'secret123', lastLogin: '2023-12-01', }; const result = Json.stringifySelected(user, ['id', 'name', 'email']); assert.isTrue(Result.isOk(result)); assert.isTrue(isString(result.value)); const parsed: unknown = JSON.parse(result.value); assert.deepStrictEqual(parsed, { id: 1, name: 'Alice', email: 'alice@example.com', }); expect(parsed).not.toHaveProperty('password'); expect(parsed).not.toHaveProperty('lastLogin'); }); test('should work with nested objects', () => { const data = { users: [ { id: 1, name: 'Alice', secret: 'hidden1' }, { id: 2, name: 'Bob', secret: 'hidden2' }, ], metadata: { total: 2, page: 1, internal: 'secret' }, }; const result = Json.stringifySelected(data, [ 'users', 'id', 'name', 'metadata', 'total', ]); assert.isTrue(Result.isOk(result)); assert.isTrue(isString(result.value)); const parsed: unknown = JSON.parse(result.value); if (isRecord(parsed) && hasKey(parsed, 'users')) { assert.isTrue(Arr.isArray(parsed.users)); expect(parsed.users).toHaveLength(2); if (Arr.isArray(parsed.users)) { assert.deepStrictEqual(parsed.users[0], { id: 1, name: 'Alice' }); expect(parsed.users[0]).not.toHaveProperty('secret'); } if (isRecord(parsed) && hasKey(parsed, 'metadata')) { assert.deepStrictEqual(parsed.metadata, { total: 2 }); expect(parsed.metadata).not.toHaveProperty('page'); expect(parsed.metadata).not.toHaveProperty('internal'); } } }); test('should work with array indices', () => { const matrix = [ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], ]; const result = Json.stringifySelected(matrix, [0, 1]); assert.isTrue(Result.isOk(result)); assert.isTrue(isString(result.value)); const parsed: unknown = JSON.parse(result.value); // Note: stringifySelected works with JSON.stringify's replacer parameter // which may not work as expected with arrays assert.isTrue(Array.isArray(parsed)); expect(parsed).toHaveLength(3); }); test('should handle formatting with space parameter', () => { const data = { a: 1, b: { c: 2 } }; const result = Json.stringifySelected(data, ['a', 'b', 'c'], 2); assert.isTrue(Result.isOk(result)); expect(result.value).toContain('\n'); expect(result.value).toContain(' '); }); test('should handle empty selection array', () => { const data = { a: 1, b: 2, c: 3 }; const result = Json.stringifySelected(data, []); assert.isTrue(Result.isOk(result)); expect(result.value).toBe('{}'); }); test('should handle undefined properties parameter', () => { const data = { a: 1, b: 2 }; const result = Json.stringifySelected(data, undefined); assert.isTrue(Result.isOk(result)); assert.isTrue(isString(result.value)); const parsed: unknown = JSON.parse(result.value); assert.deepStrictEqual(parsed, { a: 1, b: 2 }); }); test('should handle circular references with error', () => { type CircularType = { name: string; self?: CircularType }; const mut_circular: CircularType = { name: 'test' }; mut_circular.self = mut_circular; const result = Json.stringifySelected(mut_circular, ['name', 'self']); // Note: JSON.stringify may handle circular references differently depending on the replacer assert.isTrue(Result.isOk(result) || Result.isErr(result)); if (Result.isErr(result)) { expectTypeOf(result.value).toBeString(); } }); }); describe('stringifySortedKey', () => { test('should sort object keys alphabetically', () => { const unsortedObj = { zebra: 'animal', apple: 'fruit', banana: 'fruit', aardvark: 'animal', }; const result = Json.stringifySortedKey(unsortedObj); assert.isTrue(Result.isOk(result)); expect(result.value).toBe( '{"aardvark":"animal","apple":"fruit","banana":"fruit","zebra":"animal"}', ); }); test('should sort nested object keys', () => { const nestedObj = { user: { name: 'Alice', age: 30, address: { zip: '12345', city: 'New York', country: 'USA', }, }, settings: { theme: 'dark', language: 'en', }, }; const result = Json.stringifySortedKey(nestedObj); assert.isTrue(Result.isOk(result)); assert.isTrue(isString(result.value)); const parsed: unknown = JSON.parse(result.value); if (isRecord(parsed)) { const keys = Object.keys(parsed); assert.deepStrictEqual(keys, ['settings', 'user']); // sorted top-level keys if (hasKey(parsed, 'user') && isRecord(parsed.user)) { const userKeys = Object.keys(parsed.user); assert.deepStrictEqual(userKeys, ['address', 'age', 'name']); // sorted nested keys if (hasKey(parsed.user, 'address') && isRecord(parsed.user.address)) { const addressKeys = Object.keys(parsed.user.address); assert.deepStrictEqual(addressKeys, ['city', 'country', 'zip']); // sorted deeper nested keys } } } }); test('should handle arrays with objects', () => { const dataWithArrays = { users: [ { name: 'Bob', id: 2, active: true }, { name: 'Alice', id: 1, active: false }, ], metadata: { version: '1.0', created: '2023-12-01', author: 'system', }, }; const result = Json.stringifySortedKey(dataWithArrays); assert.isTrue(Result.isOk(result)); assert.isTrue(isString(result.value)); const parsed: unknown = JSON.parse(result.value); if (isRecord(parsed)) { // Check top-level keys are sorted const topKeys = Object.keys(parsed); assert.deepStrictEqual(topKeys, ['metadata', 'users']); // Check metadata keys are sorted if (hasKey(parsed, 'metadata') && isRecord(parsed.metadata)) { const metadataKeys = Object.keys(parsed.metadata); assert.deepStrictEqual(metadataKeys, ['author', 'created', 'version']); } // Check user object keys are sorted if ( hasKey(parsed, 'users') && Arr.isArray(parsed.users) && Arr.isNonEmpty(parsed.users) ) { const firstUser = parsed.users[0]; if (isRecord(firstUser)) { const userKeys = Object.keys(firstUser); assert.deepStrictEqual(userKeys, ['active', 'id', 'name']); } } } }); test('should handle formatting with space parameter', () => { const obj = { b: 2, a: 1 }; const result = Json.stringifySortedKey(obj, 2); assert.isTrue(Result.isOk(result)); expect(result.value).toContain('\n'); expect(result.value).toContain(' '); expect(result.value).toMatch(/\{\s+"a": 1,\s+"b": 2\s+\}/u); }); test('should produce deterministic output', () => { const obj1 = { c: 3, a: 1, b: 2 }; const obj2 = { b: 2, a: 1, c: 3 }; const result1 = Json.stringifySortedKey(obj1); const result2 = Json.stringifySortedKey(obj2); assert.isTrue(Result.isOk(result1)); assert.isTrue(Result.isOk(result2)); expect(result1.value).toBe(result2.value); }); test('should handle problematic objects', () => { try { type CircularObj = { normal: string; circular: { self?: CircularObj }; }; const mut_problematicObj: CircularObj = { normal: 'value', circular: {}, }; mut_problematicObj.circular.self = mut_problematicObj; const result = Json.stringifySortedKey(mut_problematicObj); // This may throw due to circular reference during key extraction assert.isTrue(Result.isErr(result)); if (Result.isErr(result)) { expectTypeOf(result.value).toBeString(); } } catch (error) { // Expected if circular reference causes stack overflow expect(error).toBeDefined(); } }); test('should handle empty object', () => { const result = Json.stringifySortedKey({}); assert.isTrue(Result.isOk(result)); expect(result.value).toBe('{}'); }); test('should handle deeply nested structures', () => { const deep = { level1: { z: 'last', a: { nested: { y: 2, x: 1, }, }, }, }; const result = Json.stringifySortedKey(deep); assert.isTrue(Result.isOk(result)); assert.isTrue(isString(result.value)); const parsed: unknown = JSON.parse(result.value); if (isRecord(parsed) && hasKey(parsed, 'level1')) { const level1 = parsed.level1; if (isRecord(level1)) { assert.deepStrictEqual(Object.keys(level1), ['a', 'z']); if ( hasKey(level1, 'a') && isRecord(level1.a) && hasKey(level1.a, 'nested') ) { const nested = level1.a.nested; if (isRecord(nested)) { assert.deepStrictEqual(Object.keys(nested), ['x', 'y']); } } } } }); });