JSON AST first cut

This commit is contained in:
Lentil Hoffman 2025-06-21 22:13:20 -05:00
parent a8d8e71885
commit 9f27912c4a
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
5 changed files with 375 additions and 0 deletions

172
__tests__/json-ast.test.ts Normal file
View File

@ -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);
});
});

18
plans/README.md Normal file
View File

@ -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

View File

@ -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);

114
src/util/json-ast/index.ts Normal file
View File

@ -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));
}

View File

@ -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
}