From 9f27912c4a00f9ffc47291a2e61830d78c274b99 Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Sat, 21 Jun 2025 22:13:20 -0500 Subject: [PATCH] JSON AST first cut --- __tests__/json-ast.test.ts | 172 +++++++++++++++++++++++++++++++++++ plans/README.md | 18 ++++ src/util/json-ast/example.ts | 50 ++++++++++ src/util/json-ast/index.ts | 114 +++++++++++++++++++++++ src/util/json-ast/types.ts | 21 +++++ 5 files changed, 375 insertions(+) create mode 100644 __tests__/json-ast.test.ts create mode 100644 plans/README.md create mode 100644 src/util/json-ast/example.ts create mode 100644 src/util/json-ast/index.ts create mode 100644 src/util/json-ast/types.ts diff --git a/__tests__/json-ast.test.ts b/__tests__/json-ast.test.ts new file mode 100644 index 0000000..bf80941 --- /dev/null +++ b/__tests__/json-ast.test.ts @@ -0,0 +1,172 @@ +import { jsonToAst } from '../src/util/json-ast'; +import { JsonNode } from '../src/util/json-ast/types'; + +describe('jsonToAst', () => { + it('should handle primitive values', () => { + expect(jsonToAst(42)).toMatchObject({ + type: 'number', + value: 42 + }); + + expect(jsonToAst('test')).toMatchObject({ + type: 'string', + value: 'test' + }); + + expect(jsonToAst(true)).toMatchObject({ + type: 'boolean', + value: true + }); + + expect(jsonToAst(null)).toMatchObject({ + type: 'null', + value: null + }); + }); + + it('should handle empty objects and arrays', () => { + const emptyObj = jsonToAst({}); + expect(emptyObj).toMatchObject({ + type: 'object', + children: [] + }); + + const emptyArray = jsonToAst([]); + expect(emptyArray).toMatchObject({ + type: 'array', + children: [] + }); + }); + + it('should handle nested objects', () => { + const ast = jsonToAst({ + name: 'test', + nested: { value: 42 } + }); + + expect(ast.type).toBe('object'); + expect(ast.children).toHaveLength(2); + + const nameNode = ast.children?.[0]; + const nestedNode = ast.children?.[1]; + + expect(nameNode).toMatchObject({ + type: 'string', + key: 'name', + value: 'test' + }); + + expect(nestedNode).toMatchObject({ + type: 'object', + key: 'nested' + }); + + expect(nestedNode?.children?.[0]).toMatchObject({ + type: 'number', + key: 'value', + value: 42 + }); + }); + + it('should handle arrays', () => { + const ast = jsonToAst([1, 'two', true]); + + expect(ast.type).toBe('array'); + expect(ast.children).toHaveLength(3); + + expect(ast.children?.[0]).toMatchObject({ + type: 'number', + value: 1 + }); + + expect(ast.children?.[1]).toMatchObject({ + type: 'string', + value: 'two' + }); + + expect(ast.children?.[2]).toMatchObject({ + type: 'boolean', + value: true + }); + }); + + it('should include paths when includePath is true', () => { + const ast = jsonToAst({ + user: { + name: 'test', + roles: ['admin', 'user'] + } + }, { includePath: true }); + + const findNode = (node: JsonNode, key: string): JsonNode | undefined => { + if (node.key === key) return node; + if (!node.children) return undefined; + for (const child of node.children) { + const found = findNode(child, key); + if (found) return found; + } + return undefined; + }; + + const nameNode = findNode(ast, 'name'); + const rolesNode = findNode(ast, 'roles'); + + expect(nameNode?.path).toBe('user.name'); + expect(rolesNode?.path).toBe('user.roles'); + expect(rolesNode?.children?.[0].path).toBe('user.roles[0]'); + }); + + it('should respect maxDepth option', () => { + const deepObject = { + level1: { + level2: { + level3: { + value: 'too deep' + } + } + } + }; + + const ast = jsonToAst(deepObject, { + maxDepth: 2, + includePath: true + }); + + const level2 = ast.children?.[0].children?.[0]; + expect(level2?.type).toBe('object'); + // The implementation sets value to undefined when max depth is exceeded + expect(level2?.value).toBeUndefined(); + expect(level2?.path).toBe('level1.level2'); + }); + + it('should apply filter function when provided', () => { + const data = { + name: 'test', + age: 42, + active: true, + address: { + street: '123 Main St', + city: 'Anytown' + } + }; + + // Only include string and number values + const ast = jsonToAst(data, { + filter: (node: JsonNode) => + node.type === 'string' || + node.type === 'number' || + node.type === 'object' // Keep objects to maintain structure + }); + + // Should have filtered out the boolean 'active' field + expect(ast.children).toHaveLength(3); + expect(ast.children?.some((c: any) => c.key === 'active')).toBe(false); + + // Nested object should only have string properties + const addressNode = ast.children?.find((c: any) => c.key === 'address'); + expect(addressNode?.children).toHaveLength(2); + expect(addressNode?.children?.every((c: any) => + c.type === 'string' || c.key === 'city' || c.key === 'street' + )).toBe(true); + }); +}); diff --git a/plans/README.md b/plans/README.md new file mode 100644 index 0000000..3074175 --- /dev/null +++ b/plans/README.md @@ -0,0 +1,18 @@ +# Project Initiatives + +This directory contains planning documents and proposals for Rhizome Node development initiatives. + +## Purpose + +- Document project goals and roadmaps +- Propose and discuss new features +- Plan research and development efforts + +## Naming Conventions + +- Use kebab-case for all filenames (e.g., `distributed-sync-research.md`) +- Include dates in filenames for time-sensitive documents (e.g., `2025-06-peer-discovery-proposal.md`) + +## Contributing + +1. Create a new markdown file for your proposal or research diff --git a/src/util/json-ast/example.ts b/src/util/json-ast/example.ts new file mode 100644 index 0000000..acfb98c --- /dev/null +++ b/src/util/json-ast/example.ts @@ -0,0 +1,50 @@ +import { jsonToAst } from './index'; + +// Example JSON data +const exampleJson = { + name: "John Doe", + age: 42, + active: true, + scores: [95, 87, 92], + address: { + street: "123 Main St", + city: "Anytown", + coordinates: { + lat: 42.1234, + lng: -71.2345 + } + }, + tags: ["admin", "user", "premium"], + metadata: { + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-06-21T12:34:56Z" + } +}; + +// Convert JSON to AST with path information +const ast = jsonToAst(exampleJson, { + includePath: true, + maxDepth: 10, + // Optional filter - only include nodes with paths that include 'address' + // filter: (node) => !node.path || node.path.includes('address') +}); + +// Print the AST +console.log(JSON.stringify(ast, null, 2)); + +// Example of traversing the AST +function traverse(node: any, indent = 0) { + const padding = ' '.repeat(indent); + const type = node.type.toUpperCase(); + const value = node.value !== undefined ? `: ${JSON.stringify(node.value)}` : ''; + const path = node.path ? ` [${node.path}]` : ''; + + console.log(`${padding}${type}${value}${path}`); + + if (node.children) { + node.children.forEach((child: any) => traverse(child, indent + 2)); + } +} + +console.log('\nTraversed AST:'); +traverse(ast); diff --git a/src/util/json-ast/index.ts b/src/util/json-ast/index.ts new file mode 100644 index 0000000..94a2785 --- /dev/null +++ b/src/util/json-ast/index.ts @@ -0,0 +1,114 @@ +import { JsonValue, JsonNode, JsonAstOptions } from './types'; + +/** + * Convert a JSON value to an Abstract Syntax Tree (AST) + * @param json The JSON value to convert + * @param options Configuration options + * @param currentPath Internal use: current path in the JSON structure + * @param depth Internal use: current depth in the JSON structure + * @returns The root node of the AST + */ +export function jsonToAst( + json: JsonValue, + options: JsonAstOptions = {}, + currentPath: string = '', + depth: number = 0 +): JsonNode { + const { includePath = true, maxDepth = 100, filter } = options; + + // Handle max depth + if (depth > maxDepth) { + return { + type: typeof json === 'object' && json !== null ? 'object' : typeof json as any, + value: '[Max depth exceeded]', + ...(includePath && currentPath ? { path: currentPath } : {}) + }; + } + + // Handle null + if (json === null) { + return createNode('null', null, currentPath, includePath); + } + + // Handle primitive types + const type = typeof json as 'string' | 'number' | 'boolean' | 'object'; + if (type !== 'object') { + return createNode(type, json, currentPath, includePath); + } + + // Handle arrays + if (Array.isArray(json)) { + const node: JsonNode = { + type: 'array', + children: json + .map((item, index) => { + const childPath = includePath ? `${currentPath}[${index}]` : ''; + return jsonToAst(item, options, childPath, depth + 1); + }) + }; + + if (includePath && currentPath) { + node.path = currentPath; + } + + return node; + } + + // Handle objects + const children: JsonNode[] = []; + for (const [key, value] of Object.entries(json)) { + const childPath = includePath + ? currentPath ? `${currentPath}.${key}` : key + : ''; + + const childNode = jsonToAst(value, options, childPath, depth + 1); + childNode.key = key; + children.push(childNode); + } + + const node: JsonNode = { + type: 'object', + children: filter ? children.filter(filter) : children + }; + + if (includePath && currentPath) { + node.path = currentPath; + } + + return node; +} + +/** + * Create a new AST node with the given properties + */ +function createNode( + type: JsonNode['type'], + value: any, + path: string = '', + includePath: boolean = true +): JsonNode { + const node: JsonNode = { type, value }; + if (includePath && path) { + node.path = path; + } + return node; +} + +/** + * Example usage of the JSON to AST converter + */ +function exampleUsage() { + const exampleJson = { + name: "John", + age: 30, + active: true, + tags: ["admin", "user"], + address: { + street: "123 Main St", + city: "Anytown" + } + }; + + const ast = jsonToAst(exampleJson, { includePath: true }); + console.log(JSON.stringify(ast, null, 2)); +} diff --git a/src/util/json-ast/types.ts b/src/util/json-ast/types.ts new file mode 100644 index 0000000..02064a0 --- /dev/null +++ b/src/util/json-ast/types.ts @@ -0,0 +1,21 @@ +export type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { [key: string]: JsonValue }; + +export interface JsonNode { + type: 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null'; + value?: any; + children?: JsonNode[]; + key?: string; + path?: string; // Path to this node in the JSON (e.g., 'address.city') +} + +export interface JsonAstOptions { + includePath?: boolean; // Whether to include path information in nodes + maxDepth?: number; // Maximum depth to traverse + filter?: (node: JsonNode) => boolean; // Optional filter function +}