Begin to implement jsonpath

This commit is contained in:
2025-03-25 00:41:04 +01:00
parent 6f9f3f5eae
commit 7fc2956b6d
2 changed files with 391 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
package main
import (
"fmt"
"strconv"
)
// JSONStep represents a single step in a JSONPath query
type JSONStep struct {
Type StepType
Key string // For Child/RecursiveDescent
Index int // For Index (use -1 for wildcard "*")
}
type StepType int
const (
RootStep StepType = iota
ChildStep
RecursiveDescentStep
WildcardStep
IndexStep
)
func ParseJSONPath(path string) ([]JSONStep, error) {
if len(path) == 0 || path[0] != '$' {
return nil, fmt.Errorf("path must start with $")
}
path = path[1:]
steps := []JSONStep{}
i := 0
for i < len(path) {
switch path[i] {
case '.':
i++
if i < len(path) && path[i] == '.' {
// Recursive descent
i++
key, nextPos := readKey(path, i)
steps = append(steps, JSONStep{Type: RecursiveDescentStep, Key: key})
i = nextPos
} else {
// Child step or wildcard
key, nextPos := readKey(path, i)
if key == "*" {
steps = append(steps, JSONStep{Type: WildcardStep})
} else {
steps = append(steps, JSONStep{Type: ChildStep, Key: key})
}
i = nextPos
}
case '[':
// Index step
i++
indexStr, nextPos := readIndex(path, i)
if indexStr == "*" {
steps = append(steps, JSONStep{Type: IndexStep, Index: -1})
} else {
index, err := strconv.Atoi(indexStr)
if err != nil {
return nil, fmt.Errorf("invalid index: %s", indexStr)
}
steps = append(steps, JSONStep{Type: IndexStep, Index: index})
}
i = nextPos + 1 // Skip closing ]
default:
return nil, fmt.Errorf("unexpected character: %c", path[i])
}
}
return steps, nil
}
func readKey(path string, start int) (string, int) {
i := start
for ; i < len(path); i++ {
if path[i] == '.' || path[i] == '[' {
break
}
}
return path[start:i], i
}
func readIndex(path string, start int) (string, int) {
i := start
for ; i < len(path); i++ {
if path[i] == ']' {
break
}
}
return path[start:i], i
}
func EvaluateJSONPath(data interface{}, steps []JSONStep) []interface{} {
current := []interface{}{data}
for _, step := range steps {
var next []interface{}
for _, node := range current {
next = append(next, evalStep(node, step)...)
}
current = next
}
return current
}
func evalStep(node interface{}, step JSONStep) []interface{} {
switch step.Type {
case ChildStep:
return evalChild(node, step.Key)
case RecursiveDescentStep:
return evalRecursiveDescent(node, step.Key)
case WildcardStep:
return evalWildcard(node)
case IndexStep:
return evalIndex(node, step.Index)
default:
return nil
}
}
func evalChild(node interface{}, key string) []interface{} {
if m, ok := node.(map[string]interface{}); ok {
if val, exists := m[key]; exists {
return []interface{}{val}
}
}
return nil
}
func evalRecursiveDescent(node interface{}, targetKey string) []interface{} {
results := []interface{}{}
queue := []interface{}{node}
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
if m, ok := current.(map[string]interface{}); ok {
// Check if current level has target key
if val, exists := m[targetKey]; exists {
results = append(results, val)
}
// Add all children to queue
for _, v := range m {
queue = append(queue, v)
}
} else if arr, ok := current.([]interface{}); ok {
queue = append(queue, arr...)
}
}
return results
}
func evalWildcard(node interface{}) []interface{} {
if m, ok := node.(map[string]interface{}); ok {
results := make([]interface{}, 0, len(m))
for _, v := range m {
results = append(results, v)
}
return results
}
return nil
}
func evalIndex(node interface{}, index int) []interface{} {
if arr, ok := node.([]interface{}); ok {
if index == -1 { // Wildcard [*]
return arr
}
if index >= 0 && index < len(arr) {
return []interface{}{arr[index]}
}
}
return nil
}

View File

@@ -0,0 +1,209 @@
package jsonpath
import (
"reflect"
"testing"
)
var testData = map[string]interface{}{
"store": map[string]interface{}{
"book": []interface{}{
map[string]interface{}{
"title": "The Fellowship of the Ring",
"price": 22.99,
},
map[string]interface{}{
"title": "The Two Towers",
"price": 23.45,
},
},
"bicycle": map[string]interface{}{
"color": "red",
"price": 199.95,
},
},
}
func TestParser(t *testing.T) {
tests := []struct {
path string
steps []JSONStep
wantErr bool
}{
{
path: "$.store.bicycle.color",
steps: []JSONStep{
{Type: RootStep},
{Type: ChildStep, Key: "store"},
{Type: ChildStep, Key: "bicycle"},
{Type: ChildStep, Key: "color"},
},
},
{
path: "$..price",
steps: []JSONStep{
{Type: RootStep},
{Type: RecursiveDescentStep, Key: "price"},
},
},
{
path: "$.store.book[*].title",
steps: []JSONStep{
{Type: RootStep},
{Type: ChildStep, Key: "store"},
{Type: ChildStep, Key: "book"},
{Type: IndexStep, Index: -1}, // Wildcard
{Type: ChildStep, Key: "title"},
},
},
{
path: "$.store.book[0]",
steps: []JSONStep{
{Type: RootStep},
{Type: ChildStep, Key: "store"},
{Type: ChildStep, Key: "book"},
{Type: IndexStep, Index: 0},
},
},
{
path: "invalid.path",
wantErr: true,
},
{
path: "$.store.book[abc]",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
steps, err := ParseJSONPath(tt.path)
if (err != nil) != tt.wantErr {
t.Fatalf("ParseJSONPath() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && !reflect.DeepEqual(steps, tt.steps) {
t.Errorf("ParseJSONPath() steps = %+v, want %+v", steps, tt.steps)
}
})
}
}
func TestEvaluator(t *testing.T) {
tests := []struct {
name string
path string
expected []interface{}
}{
{
name: "simple_property_access",
path: "$.store.bicycle.color",
expected: []interface{}{"red"},
},
{
name: "array_index_access",
path: "$.store.book[0].title",
expected: []interface{}{"The Fellowship of the Ring"},
},
{
name: "wildcard_array_access",
path: "$.store.book[*].title",
expected: []interface{}{
"The Fellowship of the Ring",
"The Two Towers",
},
},
{
name: "recursive_price_search",
path: "$..price",
expected: []interface{}{
22.99,
23.45,
199.95,
},
},
{
name: "wildcard_recursive",
path: "$..*",
expected: []interface{}{
testData["store"], // Root element
// Store children
testData["store"].(map[string]interface{})["book"],
testData["store"].(map[string]interface{})["bicycle"],
// Books
testData["store"].(map[string]interface{})["book"].([]interface{})[0],
testData["store"].(map[string]interface{})["book"].([]interface{})[1],
// Book contents
"The Fellowship of the Ring",
22.99,
"The Two Towers",
23.45,
// Bicycle
testData["store"].(map[string]interface{})["bicycle"].(map[string]interface{})["color"],
testData["store"].(map[string]interface{})["bicycle"].(map[string]interface{})["price"],
"red",
199.95,
},
},
{
name: "invalid_index",
path: "$.store.book[5]",
expected: []interface{}{},
},
{
name: "nonexistent_property",
path: "$.store.nonexistent",
expected: []interface{}{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
steps, err := ParseJSONPath(tt.path)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
result := EvaluateJSONPath(testData, steps)
// Special handling for wildcard recursive test
if tt.name == "wildcard_recursive" {
if len(result) != len(tt.expected) {
t.Errorf("Expected %d items, got %d", len(tt.expected), len(result))
}
return
}
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("EvaluateJSONPath() = %v, want %v", result, tt.expected)
}
})
}
}
func TestEdgeCases(t *testing.T) {
t.Run("empty_data", func(t *testing.T) {
steps, _ := ParseJSONPath("$.a.b")
result := EvaluateJSONPath(nil, steps)
if len(result) > 0 {
t.Errorf("Expected empty result, got %v", result)
}
})
t.Run("empty_path", func(t *testing.T) {
_, err := ParseJSONPath("")
if err == nil {
t.Error("Expected error for empty path")
}
})
t.Run("numeric_keys", func(t *testing.T) {
data := map[string]interface{}{
"42": "answer",
}
steps, _ := ParseJSONPath("$.42")
result := EvaluateJSONPath(data, steps)
if result[0] != "answer" {
t.Errorf("Expected 'answer', got %v", result)
}
})
}