Begin to implement jsonpath
This commit is contained in:
182
processor/jsonpath/jsonpath.go
Normal file
182
processor/jsonpath/jsonpath.go
Normal 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
|
||||||
|
}
|
209
processor/jsonpath/jsonpath_test.go
Normal file
209
processor/jsonpath/jsonpath_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Reference in New Issue
Block a user