From 7fc2956b6d9afb78de62051969fa81f9930cea6a Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Tue, 25 Mar 2025 00:41:04 +0100 Subject: [PATCH] Begin to implement jsonpath --- processor/jsonpath/jsonpath.go | 182 ++++++++++++++++++++++++ processor/jsonpath/jsonpath_test.go | 209 ++++++++++++++++++++++++++++ 2 files changed, 391 insertions(+) create mode 100644 processor/jsonpath/jsonpath.go create mode 100644 processor/jsonpath/jsonpath_test.go diff --git a/processor/jsonpath/jsonpath.go b/processor/jsonpath/jsonpath.go new file mode 100644 index 0000000..3ac082c --- /dev/null +++ b/processor/jsonpath/jsonpath.go @@ -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 +} diff --git a/processor/jsonpath/jsonpath_test.go b/processor/jsonpath/jsonpath_test.go new file mode 100644 index 0000000..82d8cba --- /dev/null +++ b/processor/jsonpath/jsonpath_test.go @@ -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) + } + }) +} \ No newline at end of file