UNPKG

jsonfieldexplorer

Version:

Node.js tool to efficiently explore and list all field paths in a JSON object. Perfect for understanding complex JSON structures, it recursively analyzes JSON data to provide a clear summary of nested fields and arrays.

415 lines (356 loc) 13.3 kB
import assert from "assert"; import { processJson, summarizePaths, pathsToLines } from "../jfe.js"; import { filterLines, sortLines, extractType } from "../interactive.js"; describe("summarizePaths", () => { it("should handle simple objects", () => { const input = { a: 1, b: "hello" }; const expected = { ".a": [1], ".b": ["hello"], }; assert.deepEqual(summarizePaths(input), expected); }); it("should handle nested objects", () => { const input = { a: { b: 2, c: [3, 4] } }; const expected = { ".a": [{ b: 2, c: [3, 4] }], ".a.b": [2], ".a.c": [[3, 4]], ".a.c[]": [3, 4], }; assert.deepEqual(summarizePaths(input), expected); }); it("should handle arrays", () => { const input = { a: [1, 2, 3] }; const expected = { ".a": [[1, 2, 3]], ".a[]": [1, 2, 3], }; const result = summarizePaths(input); assert.deepEqual(JSON.stringify(result), JSON.stringify(expected)); }); it("should handle null values", () => { const input = { a: null }; const expected = { ".a": [null], }; assert.deepEqual(summarizePaths(input), expected); }); it("should handle spaces in keys", () => { const input = { "a b": 1 }; const expected = { '."a b"': [1], }; assert.deepEqual(summarizePaths(input), expected); }); it("should handle top-level arrays", () => { const input = [{ a: 1 }, { a: 2 }]; const result = summarizePaths(input); assert.ok(result["[]"]); assert.ok(result["[].a"]); assert.equal(result["[]"].length, 1); assert.equal(result["[].a"].length, 2); }); it("should detect optional fields in arrays", () => { const input = [ { field1: "string", field2: 123 }, { field1: "string", field2: 123, field3: true }, ]; const result = summarizePaths(input); // Should have array context for optional field detection assert.ok(result._arrayContexts); assert.ok(result._arrayContexts["[].field3"]); // field3 should be present in only 1 out of 2 elements const context = result._arrayContexts["[].field3"]; assert.equal(context.totalElements, 2); assert.equal(context.fieldPresence.get("field3").length, 1); }); }); describe("processJson", () => { it("should process simple JSON", () => { const data = { a: 1, b: "hello" }; const expectedOutput = [".a: number", ".b: string"]; const consoleLogSpy = jest .spyOn(console, "log") .mockImplementation(() => {}); processJson(data); const loggedLines = consoleLogSpy.mock.calls.map((call) => call.join(" ")); const got = JSON.stringify(loggedLines); const expected = JSON.stringify(expectedOutput); assert.equal(got, expected); consoleLogSpy.mockRestore(); }); it("should show optional fields in output", () => { const data = [ { field1: "string", field2: 123 }, { field1: "string", field2: 123, field3: true }, ]; const expectedOutput = [ "[]: array (size: 2)", "[].field1: string", "[].field2: number", "[].field3: boolean | optional", ]; const consoleLogSpy = jest .spyOn(console, "log") .mockImplementation(() => {}); processJson(data); const loggedLines = consoleLogSpy.mock.calls.map((call) => call.join(" ")); const got = JSON.stringify(loggedLines); const expected = JSON.stringify(expectedOutput); assert.equal(got, expected); consoleLogSpy.mockRestore(); }); }); describe("Enum Detection", () => { it("should detect string enums with few unique values", () => { const input = [ { status: "active" }, { status: "inactive" }, { status: "active" }, { status: "pending" } ]; const result = summarizePaths(input); const lines = pathsToLines(result); // Should detect enum for status field const statusLine = lines.find(line => line.includes("[].status")); assert.ok(statusLine.includes('enum ["active", "inactive", "pending"] (3 values)')); }); it("should detect number enums", () => { const input = [ { priority: 1 }, { priority: 2 }, { priority: 1 }, { priority: 3 } ]; const result = summarizePaths(input); const lines = pathsToLines(result); // Should detect enum for priority field const priorityLine = lines.find(line => line.includes("[].priority")); assert.ok(priorityLine.includes('enum [1, 2, 3] (3 values)')); }); it("should detect boolean enums", () => { const input = [ { active: true }, { active: false }, { active: true } ]; const result = summarizePaths(input); const lines = pathsToLines(result); // Should detect enum for active field const activeLine = lines.find(line => line.includes("[].active")); assert.ok(activeLine.includes('enum [false, true] (2 values)')); }); it("should not show enum for single unique value", () => { const input = [ { status: "active" }, { status: "active" }, { status: "active" } ]; const result = summarizePaths(input); const lines = pathsToLines(result); // Should not detect enum, just show type const statusLine = lines.find(line => line.includes("[].status")); assert.ok(statusLine.includes("string")); assert.ok(!statusLine.includes("enum")); }); it("should not show enum for too many unique values", () => { const input = Array.from({ length: 12 }, (_, i) => ({ id: i })); const result = summarizePaths(input); const lines = pathsToLines(result, { maxEnumValues: 10 }); // Should not detect enum because there are too many unique values const idLine = lines.find(line => line.includes("[].id")); assert.ok(idLine.includes("number")); assert.ok(!idLine.includes("enum")); }); it("should respect maxEnumValues option", () => { const input = [ { level: 1 }, { level: 2 }, { level: 3 }, { level: 4 }, { level: 5 } ]; const result = summarizePaths(input); // With default maxEnumValues (10), should show enum const lines1 = pathsToLines(result, { maxEnumValues: 10 }); const levelLine1 = lines1.find(line => line.includes("[].level")); assert.ok(levelLine1.includes("enum")); // With maxEnumValues set to 3, should not show enum (5 > 3) const lines2 = pathsToLines(result, { maxEnumValues: 3 }); const levelLine2 = lines2.find(line => line.includes("[].level")); assert.ok(!levelLine2.includes("enum")); }); it("should sort enum values consistently", () => { const input = [ { status: "zebra" }, { status: "alpha" }, { status: "beta" } ]; const result = summarizePaths(input); const lines = pathsToLines(result); // Should be sorted alphabetically const statusLine = lines.find(line => line.includes("[].status")); assert.ok(statusLine.includes('enum ["alpha", "beta", "zebra"]')); }); }); describe("Statistics Mode", () => { it("should show number statistics", () => { const input = [ { score: 85 }, { score: 92 }, { score: 78 }, { score: 95 } ]; const result = summarizePaths(input); const lines = pathsToLines(result, { stats: true }); const scoreLine = lines.find(line => line.includes("[].score")); assert.ok(scoreLine.includes("number (4 total")); assert.ok(scoreLine.includes("min: 78")); assert.ok(scoreLine.includes("max: 95")); assert.ok(scoreLine.includes("avg: 87.50")); assert.ok(scoreLine.includes("sum: 350")); }); it("should show string statistics", () => { const input = [ { name: "Alice" }, { name: "Bob" }, { name: "Alice" }, { name: "Charlie" } ]; const result = summarizePaths(input); const lines = pathsToLines(result, { stats: true }); const nameLine = lines.find(line => line.includes("[].name")); assert.ok(nameLine.includes("string (4 total")); assert.ok(nameLine.includes("unique: 3")); assert.ok(nameLine.includes('most common: "Alice" (2x)')); }); it("should show boolean statistics", () => { const input = [ { active: true }, { active: false }, { active: true }, { active: true } ]; const result = summarizePaths(input); const lines = pathsToLines(result, { stats: true }); const activeLine = lines.find(line => line.includes("[].active")); assert.ok(activeLine.includes("boolean (4 total")); assert.ok(activeLine.includes("true: 3")); assert.ok(activeLine.includes("false: 1")); }); it("should handle null values in statistics", () => { const input = [ { score: 85 }, { score: null }, { score: 92 }, { score: null } ]; const result = summarizePaths(input); const lines = pathsToLines(result, { stats: true }); const scoreLine = lines.find(line => line.includes("[].score")); assert.ok(scoreLine.includes("number (2 total")); // Only counts non-null values assert.ok(scoreLine.includes("min: 85")); assert.ok(scoreLine.includes("max: 92")); }); it("should show enum instead of stats by default", () => { const input = [ { status: "active" }, { status: "inactive" }, { status: "active" } ]; const result = summarizePaths(input); // Without stats flag, should show enum const normalLines = pathsToLines(result, { stats: false }); const normalLine = normalLines.find(line => line.includes("[].status")); assert.ok(normalLine.includes("enum")); // With stats flag, should show statistics const statsLines = pathsToLines(result, { stats: true }); const statsLine = statsLines.find(line => line.includes("[].status")); assert.ok(statsLine.includes("string (3 total")); assert.ok(statsLine.includes("unique: 2")); }); it("should handle edge case with all null values", () => { const input = [ { value: null }, { value: null }, { value: null } ]; const result = summarizePaths(input); const lines = pathsToLines(result, { stats: true }); const valueLine = lines.find(line => line.includes("[].value")); // When all values are null, it shows as "null" type, not statistics assert.ok(valueLine.includes("null")); }); }); describe("Interactive Mode Helpers", () => { const sampleLines = [ ".users: array (size: 3)", ".users[]: object", ".users[].name: string", ".users[].age: number", ".users[].profile: object", ".users[].profile.city: string", ".users[].active: boolean" ]; describe("filterLines", () => { it("should return all lines when no filter provided", () => { const result = filterLines(sampleLines, ""); assert.equal(result.length, sampleLines.length); }); it("should filter lines with simple string matching", () => { const result = filterLines(sampleLines, "profile"); assert.equal(result.length, 2); // .profile and .profile.city }); it("should support regex filtering", () => { const result = filterLines(sampleLines, "users\\[\\]\\.(name|age):"); assert.equal(result.length, 2); // name and age }); it("should be case insensitive", () => { const result = filterLines(sampleLines, "ARRAY"); assert.equal(result.length, 1); }); it("should fallback to string contains on invalid regex", () => { const result = filterLines(sampleLines, "[invalid"); // Invalid regex assert.equal(result.length, 0); // Should not crash }); }); describe("sortLines", () => { it("should sort by path depth by default", () => { const result = sortLines(sampleLines, "path"); assert.ok(result[0].includes(".users:")); assert.ok(result[result.length - 1].includes(".users[].profile.city")); }); it("should sort alphabetically", () => { const result = sortLines(sampleLines, "alpha"); assert.ok(result[0].includes(".users:")); assert.ok(result[1].includes(".users[]:")); }); it("should sort by type", () => { const result = sortLines(sampleLines, "type"); // Should group by type, then alphabetically const arrayLines = result.filter(line => line.includes("array")); const booleanLines = result.filter(line => line.includes("boolean")); assert.equal(arrayLines.length, 1); assert.equal(booleanLines.length, 1); }); it("should not modify original array", () => { const original = [...sampleLines]; sortLines(sampleLines, "alpha"); assert.deepEqual(sampleLines, original); }); }); describe("extractType", () => { it("should extract type from field line", () => { assert.equal(extractType(".users[].name: string"), "string"); assert.equal(extractType(".users[].age: number"), "number"); assert.equal(extractType(".users: array (size: 3)"), "array"); }); it("should handle enum types", () => { assert.equal(extractType('.users[].status: enum ["active", "inactive"]'), "enum"); }); it("should return unknown for invalid format", () => { assert.equal(extractType("invalid line"), "unknown"); }); }); });