JSON AST first cut
This commit is contained in:
parent
a8d8e71885
commit
9f27912c4a
172
__tests__/json-ast.test.ts
Normal file
172
__tests__/json-ast.test.ts
Normal 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
18
plans/README.md
Normal 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
|
50
src/util/json-ast/example.ts
Normal file
50
src/util/json-ast/example.ts
Normal 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
114
src/util/json-ast/index.ts
Normal 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));
|
||||||
|
}
|
21
src/util/json-ast/types.ts
Normal file
21
src/util/json-ast/types.ts
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user