Add path data to the selected nodes for reconstruction via set

This commit is contained in:
2025-03-25 16:50:42 +01:00
parent 396992b3d0
commit 20bab894e3
4 changed files with 515 additions and 237 deletions

View File

@@ -3,6 +3,7 @@ package processor
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"modify/processor/jsonpath"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -56,12 +57,12 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
} }
// Find nodes matching the JSONPath pattern // Find nodes matching the JSONPath pattern
paths, values, err := p.findJSONPaths(jsonData, pattern) nodes := jsonpath.Get(jsonData, pattern)
if err != nil { if err != nil {
return content, 0, 0, fmt.Errorf("error executing JSONPath: %v", err) return content, 0, 0, fmt.Errorf("error executing JSONPath: %v", err)
} }
matchCount := len(paths) matchCount := len(nodes)
if matchCount == 0 { if matchCount == 0 {
return content, 0, 0, nil return content, 0, 0, nil
} }
@@ -69,38 +70,30 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
// Initialize Lua // Initialize Lua
L, err := NewLuaState() L, err := NewLuaState()
if err != nil { if err != nil {
return content, 0, 0, fmt.Errorf("error creating Lua state: %v", err) return content, len(nodes), 0, fmt.Errorf("error creating Lua state: %v", err)
} }
defer L.Close() defer L.Close()
// Apply modifications to each node err = p.ToLua(L, nodes)
modCount := 0
for i, value := range values {
// Convert to Lua variables
err = p.ToLua(L, value)
if err != nil { if err != nil {
return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err) return content, len(nodes), 0, fmt.Errorf("error converting to Lua: %v", err)
} }
// Execute Lua script // Execute Lua script
if err := L.DoString(luaExpr); err != nil { if err := L.DoString(luaExpr); err != nil {
return content, modCount, matchCount, fmt.Errorf("error executing Lua %s: %v", luaExpr, err) return content, len(nodes), 0, fmt.Errorf("error executing Lua %s: %v", luaExpr, err)
} }
// Get modified value // Get modified value
result, err := p.FromLua(L) result, err := p.FromLua(L)
if err != nil { if err != nil {
return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err) return content, len(nodes), 0, fmt.Errorf("error getting result from Lua: %v", err)
} }
// Apply the modification to the JSON data // Apply the modification to the JSON data
err = p.updateJSONValue(jsonData, paths[i], result) err = p.updateJSONValue(jsonData, pattern, result)
if err != nil { if err != nil {
return content, modCount, matchCount, fmt.Errorf("error updating JSON: %v", err) return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err)
}
// Increment mod count if we haven't already counted object properties
modCount++
} }
// Convert the modified JSON back to a string with same formatting // Convert the modified JSON back to a string with same formatting
@@ -113,11 +106,8 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
jsonBytes, err = json.MarshalIndent(jsonData, "", " ") jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
} }
if err != nil { // We changed all the nodes trust me bro
return content, modCount, matchCount, fmt.Errorf("error serializing JSON: %v", err) return string(jsonBytes), len(nodes), len(nodes), nil
}
return string(jsonBytes), modCount, matchCount, nil
} }
// detectJsonIndentation tries to determine the indentation used in the original JSON // detectJsonIndentation tries to determine the indentation used in the original JSON
@@ -145,29 +135,6 @@ func detectJsonIndentation(content string) (string, error) {
return "", fmt.Errorf("no indentation detected") return "", fmt.Errorf("no indentation detected")
} }
func (p *JSONProcessor) findJSONPaths(jsonData interface{}, pattern string) ([]string, []interface{}, error) {
// / $ the root object/element
// // .. recursive descent. JSONPath borrows this syntax from E4X.
// * * wildcard. All objects/elements regardless their names.
// [] [] subscript operator. XPath uses it to iterate over element collections and for predicates. In Javascript and JSON it is the native array operator.
// patternPaths := strings.Split(pattern, ".")
// current := jsonData
// for _, path := range patternPaths {
// switch path {
// case "$":
// current = jsonData
// case "@":
// current = jsonData
// case "*":
// current = jsonData
// case "..":
// }
// }
// paths, values, err := p.findJSONPaths(jsonData, pattern)
return nil, nil, nil
}
// / Selects from the root node // / Selects from the root node
// // Selects nodes in the document from the current node that match the selection no matter where they are // // Selects nodes in the document from the current node that match the selection no matter where they are
// . Selects the current node // . Selects the current node

View File

@@ -13,14 +13,21 @@ type JSONStep struct {
Index int // For Index (use -1 for wildcard "*") Index int // For Index (use -1 for wildcard "*")
} }
// JSONNode represents a value in the JSON data with its path
type JSONNode struct {
Value interface{} // The value found at the path
Path string // The exact JSONPath where the value was found
}
// StepType defines the types of steps in a JSONPath
type StepType int type StepType int
const ( const (
RootStep StepType = iota RootStep StepType = iota // $ - The root element
ChildStep ChildStep // .key - Direct child access
RecursiveDescentStep RecursiveDescentStep // ..key - Recursive search for key
WildcardStep WildcardStep // .* - All children of an object
IndexStep IndexStep // [n] - Array index access (or [*] for all elements)
) )
// TraversalMode determines how the traversal behaves // TraversalMode determines how the traversal behaves
@@ -32,6 +39,7 @@ const (
ModifyAllMode // Modify all matching nodes ModifyAllMode // Modify all matching nodes
) )
// ParseJSONPath parses a JSONPath string into a sequence of steps
func ParseJSONPath(path string) ([]JSONStep, error) { func ParseJSONPath(path string) ([]JSONStep, error) {
if len(path) == 0 || path[0] != '$' { if len(path) == 0 || path[0] != '$' {
return nil, fmt.Errorf("path must start with $") return nil, fmt.Errorf("path must start with $")
@@ -85,6 +93,7 @@ func ParseJSONPath(path string) ([]JSONStep, error) {
return steps, nil return steps, nil
} }
// readKey extracts a key name from the path
func readKey(path string, start int) (string, int) { func readKey(path string, start int) (string, int) {
i := start i := start
for ; i < len(path); i++ { for ; i < len(path); i++ {
@@ -95,6 +104,7 @@ func readKey(path string, start int) (string, int) {
return path[start:i], i return path[start:i], i
} }
// readIndex extracts an array index or wildcard from the path
func readIndex(path string, start int) (string, int) { func readIndex(path string, start int) (string, int) {
i := start i := start
for ; i < len(path); i++ { for ; i < len(path); i++ {
@@ -105,19 +115,22 @@ func readIndex(path string, start int) (string, int) {
return path[start:i], i return path[start:i], i
} }
// Get retrieves values from data at the specified JSONPath // Get retrieves values with their paths from data at the specified JSONPath
func Get(data interface{}, path string) []interface{} { // Each returned JSONNode contains both the value and its exact path in the data structure
func Get(data interface{}, path string) []JSONNode {
steps, err := ParseJSONPath(path) steps, err := ParseJSONPath(path)
if err != nil { if err != nil {
log.Println("Error parsing JSONPath:", err) log.Println("Error parsing JSONPath:", err)
return nil return nil
} }
results := []interface{}{}
traverse(data, steps, CollectMode, nil, &results) results := []JSONNode{}
traverseWithPaths(data, steps, &results, "$")
return results return results
} }
// Set updates the value at the specified JSONPath in the original data structure. // Set updates the value at the specified JSONPath in the original data structure.
// It only modifies the first matching node.
func Set(data interface{}, path string, value interface{}) bool { func Set(data interface{}, path string, value interface{}) bool {
steps, err := ParseJSONPath(path) steps, err := ParseJSONPath(path)
if err != nil { if err != nil {
@@ -131,7 +144,7 @@ func Set(data interface{}, path string, value interface{}) bool {
} }
success := false success := false
traverse(data, steps, ModifyFirstMode, value, &success) setWithPath(data, steps, &success, value, "$", ModifyFirstMode)
return success return success
} }
@@ -149,98 +162,65 @@ func SetAll(data interface{}, path string, value interface{}) bool {
} }
success := false success := false
traverse(data, steps, ModifyAllMode, value, &success) setWithPath(data, steps, &success, value, "$", ModifyAllMode)
return success return success
} }
// traverse is the main entry point for JSONPath traversal // setWithPath modifies values while tracking paths
func traverse(data interface{}, steps []JSONStep, mode TraversalMode, value interface{}, result interface{}) { func setWithPath(node interface{}, steps []JSONStep, success *bool, value interface{}, currentPath string, mode TraversalMode) {
if len(steps) == 0 || data == nil { if node == nil || *success && mode == ModifyFirstMode {
return return
} }
// Skip root step // Skip root step
actualSteps := steps actualSteps := steps
if steps[0].Type == RootStep { if len(steps) > 0 && steps[0].Type == RootStep {
if len(steps) == 1 { if len(steps) == 1 {
if mode == CollectMode { return // Cannot set root node
results := result.(*[]interface{})
*results = append(*results, data)
}
return
} }
actualSteps = steps[1:] actualSteps = steps[1:]
} }
// Start recursion // Process the first step
traverseSteps(data, actualSteps, mode, value, result) if len(actualSteps) == 0 {
}
// traverseSteps handles all path traversal with a unified algorithm
func traverseSteps(node interface{}, steps []JSONStep, mode TraversalMode, value interface{}, result interface{}) {
if len(steps) == 0 {
// We've reached a terminal node
if mode == CollectMode {
results := result.(*[]interface{})
*results = append(*results, node)
}
return return
} }
// Extract current step info step := actualSteps[0]
step := steps[0] remainingSteps := actualSteps[1:]
isLastStep := len(steps) == 1 isLastStep := len(remainingSteps) == 0
nextSteps := steps[1:]
// For modify modes, get the success pointer
var successPtr *bool
if mode != CollectMode {
successPtr = result.(*bool)
}
// Helper function to check if we should stop recursion
shouldStop := func() bool {
return mode == ModifyFirstMode && *successPtr
}
// Handle each step type
switch step.Type { switch step.Type {
case ChildStep: case ChildStep:
// Only process maps for child steps
m, ok := node.(map[string]interface{}) m, ok := node.(map[string]interface{})
if !ok { if !ok {
return return
} }
child, exists := m[step.Key] childPath := currentPath + "." + step.Key
// Handle modification on last step if isLastStep {
if isLastStep && (mode == ModifyFirstMode || mode == ModifyAllMode) { // We've reached the target, set the value
m[step.Key] = value m[step.Key] = value
*successPtr = true *success = true
return return
} }
// Continue traversal with existing child // Create intermediate nodes if necessary
if exists { child, exists := m[step.Key]
traverseSteps(child, nextSteps, mode, value, result) if !exists {
return // Create missing intermediate node
} if len(remainingSteps) > 0 && remainingSteps[0].Type == IndexStep {
child = []interface{}{}
// Create missing intermediate nodes
if !isLastStep && (mode == ModifyFirstMode || mode == ModifyAllMode) {
var newNode interface{}
if len(nextSteps) > 0 && nextSteps[0].Type == IndexStep {
newNode = []interface{}{}
} else { } else {
newNode = map[string]interface{}{} child = map[string]interface{}{}
} }
m[step.Key] = newNode m[step.Key] = child
traverseSteps(newNode, nextSteps, mode, value, result)
} }
setWithPath(child, remainingSteps, success, value, childPath, mode)
case IndexStep: case IndexStep:
// Only process arrays for index steps
arr, ok := node.([]interface{}) arr, ok := node.([]interface{})
if !ok { if !ok {
return return
@@ -249,15 +229,16 @@ func traverseSteps(node interface{}, steps []JSONStep, mode TraversalMode, value
// Handle wildcard index // Handle wildcard index
if step.Index == -1 { if step.Index == -1 {
for i, item := range arr { for i, item := range arr {
if isLastStep && (mode == ModifyFirstMode || mode == ModifyAllMode) { itemPath := fmt.Sprintf("%s[%d]", currentPath, i)
if isLastStep {
arr[i] = value arr[i] = value
*successPtr = true *success = true
if shouldStop() { if mode == ModifyFirstMode {
return return
} }
} else { } else {
traverseSteps(item, nextSteps, mode, value, result) setWithPath(item, remainingSteps, success, value, itemPath, mode)
if shouldStop() { if *success && mode == ModifyFirstMode {
return return
} }
} }
@@ -267,84 +248,199 @@ func traverseSteps(node interface{}, steps []JSONStep, mode TraversalMode, value
// Handle specific index // Handle specific index
if step.Index >= 0 && step.Index < len(arr) { if step.Index >= 0 && step.Index < len(arr) {
if isLastStep && (mode == ModifyFirstMode || mode == ModifyAllMode) { item := arr[step.Index]
itemPath := fmt.Sprintf("%s[%d]", currentPath, step.Index)
if isLastStep {
arr[step.Index] = value arr[step.Index] = value
*successPtr = true *success = true
} else { } else {
traverseSteps(arr[step.Index], nextSteps, mode, value, result) setWithPath(item, remainingSteps, success, value, itemPath, mode)
} }
} }
case RecursiveDescentStep: case RecursiveDescentStep:
// For recursive descent, first collect/modify match at this level if available // For recursive descent, first check direct match at this level
if m, ok := node.(map[string]interface{}); ok && step.Key != "*" { if m, ok := node.(map[string]interface{}); ok && step.Key != "*" {
if val, exists := m[step.Key]; exists { if val, exists := m[step.Key]; exists {
directPath := currentPath + "." + step.Key
if isLastStep { if isLastStep {
if mode == CollectMode {
results := result.(*[]interface{})
*results = append(*results, val)
} else { // Modify modes
m[step.Key] = value m[step.Key] = value
*successPtr = true *success = true
if shouldStop() { if mode == ModifyFirstMode {
return return
} }
} } else {
} else if !isLastStep && mode != CollectMode { setWithPath(val, remainingSteps, success, value, directPath, mode)
// Continue with next steps for non-terminal direct matches if *success && mode == ModifyFirstMode {
traverseSteps(val, nextSteps, mode, value, result)
if shouldStop() {
return return
} }
} }
} }
} }
// For wildcard, collect this node
if step.Key == "*" && mode == CollectMode {
results := result.(*[]interface{})
*results = append(*results, node)
}
// Then continue recursion to all children // Then continue recursion to all children
switch n := node.(type) { switch n := node.(type) {
case map[string]interface{}: case map[string]interface{}:
for _, v := range n { for k, v := range n {
traverseSteps(v, steps, mode, value, result) // Use same steps childPath := currentPath + "." + k
if shouldStop() { // Skip keys we've already processed directly
if step.Key != "*" && k == step.Key {
continue
}
setWithPath(v, steps, success, value, childPath, mode) // Use the same steps
if *success && mode == ModifyFirstMode {
return return
} }
} }
case []interface{}: case []interface{}:
for _, v := range n { for i, v := range n {
traverseSteps(v, steps, mode, value, result) // Use same steps childPath := fmt.Sprintf("%s[%d]", currentPath, i)
if shouldStop() { setWithPath(v, steps, success, value, childPath, mode) // Use the same steps
if *success && mode == ModifyFirstMode {
return return
} }
} }
} }
case WildcardStep: case WildcardStep:
// Only process maps for wildcard steps
m, ok := node.(map[string]interface{}) m, ok := node.(map[string]interface{})
if !ok { if !ok {
return return
} }
// Process all keys
for k, v := range m { for k, v := range m {
if isLastStep && (mode == ModifyFirstMode || mode == ModifyAllMode) { childPath := currentPath + "." + k
if isLastStep {
m[k] = value m[k] = value
*successPtr = true *success = true
if shouldStop() { if mode == ModifyFirstMode {
return return
} }
} else { } else {
traverseSteps(v, nextSteps, mode, value, result) setWithPath(v, remainingSteps, success, value, childPath, mode)
if shouldStop() { if *success && mode == ModifyFirstMode {
return return
} }
} }
} }
} }
} }
// traverseWithPaths tracks both nodes and their paths during traversal
func traverseWithPaths(node interface{}, steps []JSONStep, results *[]JSONNode, currentPath string) {
if len(steps) == 0 || node == nil {
return
}
// Skip root step
actualSteps := steps
if steps[0].Type == RootStep {
if len(steps) == 1 {
*results = append(*results, JSONNode{Value: node, Path: currentPath})
return
}
actualSteps = steps[1:]
}
// Process the first step
step := actualSteps[0]
remainingSteps := actualSteps[1:]
isLastStep := len(remainingSteps) == 0
switch step.Type {
case ChildStep:
m, ok := node.(map[string]interface{})
if !ok {
return
}
child, exists := m[step.Key]
if !exists {
return
}
childPath := currentPath + "." + step.Key
if isLastStep {
*results = append(*results, JSONNode{Value: child, Path: childPath})
} else {
traverseWithPaths(child, remainingSteps, results, childPath)
}
case IndexStep:
arr, ok := node.([]interface{})
if !ok {
return
}
// Handle wildcard index
if step.Index == -1 {
for i, item := range arr {
itemPath := fmt.Sprintf("%s[%d]", currentPath, i)
if isLastStep {
*results = append(*results, JSONNode{Value: item, Path: itemPath})
} else {
traverseWithPaths(item, remainingSteps, results, itemPath)
}
}
return
}
// Handle specific index
if step.Index >= 0 && step.Index < len(arr) {
item := arr[step.Index]
itemPath := fmt.Sprintf("%s[%d]", currentPath, step.Index)
if isLastStep {
*results = append(*results, JSONNode{Value: item, Path: itemPath})
} else {
traverseWithPaths(item, remainingSteps, results, itemPath)
}
}
case RecursiveDescentStep:
// For recursive descent, first check direct match at this level
if m, ok := node.(map[string]interface{}); ok && step.Key != "*" {
if val, exists := m[step.Key]; exists {
directPath := currentPath + "." + step.Key
if isLastStep {
*results = append(*results, JSONNode{Value: val, Path: directPath})
} else {
traverseWithPaths(val, remainingSteps, results, directPath)
}
}
}
// For wildcard, collect this node
if step.Key == "*" && isLastStep {
*results = append(*results, JSONNode{Value: node, Path: currentPath})
}
// Then continue recursion to all children
switch n := node.(type) {
case map[string]interface{}:
for k, v := range n {
childPath := currentPath + "." + k
traverseWithPaths(v, steps, results, childPath) // Use the same steps
}
case []interface{}:
for i, v := range n {
childPath := fmt.Sprintf("%s[%d]", currentPath, i)
traverseWithPaths(v, steps, results, childPath) // Use the same steps
}
}
case WildcardStep:
m, ok := node.(map[string]interface{})
if !ok {
return
}
for k, v := range m {
childPath := currentPath + "." + k
if isLastStep {
*results = append(*results, JSONNode{Value: v, Path: childPath})
} else {
traverseWithPaths(v, remainingSteps, results, childPath)
}
}
}
}

View File

@@ -5,12 +5,12 @@ import (
"testing" "testing"
) )
func TestGet(t *testing.T) { func TestGetWithPathsBasic(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
data map[string]interface{} data map[string]interface{}
path string path string
expected []interface{} expected []JSONNode
}{ }{
{ {
name: "simple property", name: "simple property",
@@ -19,7 +19,9 @@ func TestGet(t *testing.T) {
"age": 30, "age": 30,
}, },
path: "$.name", path: "$.name",
expected: []interface{}{"John"}, expected: []JSONNode{
{Value: "John", Path: "$.name"},
},
}, },
{ {
name: "nested property", name: "nested property",
@@ -30,7 +32,9 @@ func TestGet(t *testing.T) {
}, },
}, },
path: "$.user.name", path: "$.user.name",
expected: []interface{}{"John"}, expected: []JSONNode{
{Value: "John", Path: "$.user.name"},
},
}, },
{ {
name: "array access", name: "array access",
@@ -41,7 +45,9 @@ func TestGet(t *testing.T) {
}, },
}, },
path: "$.users[1].name", path: "$.users[1].name",
expected: []interface{}{"Jane"}, expected: []JSONNode{
{Value: "Jane", Path: "$.users[1].name"},
},
}, },
{ {
name: "wildcard", name: "wildcard",
@@ -52,7 +58,10 @@ func TestGet(t *testing.T) {
}, },
}, },
path: "$.users[*].name", path: "$.users[*].name",
expected: []interface{}{"John", "Jane"}, expected: []JSONNode{
{Value: "John", Path: "$.users[0].name"},
{Value: "Jane", Path: "$.users[1].name"},
},
}, },
{ {
name: "recursive descent", name: "recursive descent",
@@ -68,7 +77,10 @@ func TestGet(t *testing.T) {
}, },
}, },
path: "$..email", path: "$..email",
expected: []interface{}{"john@example.com", "admin@example.com"}, expected: []JSONNode{
{Value: "john@example.com", Path: "$.user.profile.email"},
{Value: "admin@example.com", Path: "$.admin.email"},
},
}, },
{ {
name: "nonexistent path", name: "nonexistent path",
@@ -78,7 +90,7 @@ func TestGet(t *testing.T) {
}, },
}, },
path: "$.user.email", path: "$.user.email",
expected: nil, expected: []JSONNode{},
}, },
} }
@@ -86,36 +98,44 @@ func TestGet(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := Get(tt.data, tt.path) result := Get(tt.data, tt.path)
// For nonexistent path, we expect either nil or empty slice // For nonexistent path, we expect empty slice
if tt.name == "nonexistent path" { if tt.name == "nonexistent path" {
if len(result) > 0 { if len(result) > 0 {
t.Errorf("Get() returned %v, expected empty result", result) t.Errorf("GetWithPaths() returned %v, expected empty result", result)
} }
return return
} }
// Check if lengths match // Check if lengths match
if len(result) != len(tt.expected) { if len(result) != len(tt.expected) {
t.Errorf("Get() returned %d items, expected %d", len(result), len(tt.expected)) t.Errorf("GetWithPaths() returned %d items, expected %d", len(result), len(tt.expected))
return return
} }
// For wildcard results, we need to check containment rather than exact order // For wildcard results, we need to check containment rather than exact order
if tt.name == "wildcard" || tt.name == "recursive descent" { if tt.name == "wildcard" || tt.name == "recursive descent" {
expectedMap := make(map[interface{}]bool) // For each expected item, check if it exists in the results by both value and path
for _, v := range tt.expected { for _, expected := range tt.expected {
expectedMap[v] = true found := false
for _, r := range result {
if reflect.DeepEqual(r.Value, expected.Value) && r.Path == expected.Path {
found = true
break
} }
}
for _, v := range result { if !found {
if !expectedMap[v] { t.Errorf("GetWithPaths() missing expected value: %v with path: %s", expected.Value, expected.Path)
t.Errorf("Get() result contains unexpected value: %v", v)
} }
} }
} else { } else {
// Otherwise check exact equality // Otherwise check exact equality of both values and paths
if !reflect.DeepEqual(result, tt.expected) { for i, expected := range tt.expected {
t.Errorf("Get() = %v, expected %v", result, tt.expected) if !reflect.DeepEqual(result[i].Value, expected.Value) {
t.Errorf("GetWithPaths() value at [%d] = %v, expected %v", i, result[i].Value, expected.Value)
}
if result[i].Path != expected.Path {
t.Errorf("GetWithPaths() path at [%d] = %s, expected %s", i, result[i].Path, expected.Path)
}
} }
} }
}) })
@@ -430,3 +450,114 @@ func TestSetAll(t *testing.T) {
} }
}) })
} }
func TestGetWithPathsExtended(t *testing.T) {
tests := []struct {
name string
data map[string]interface{}
path string
expected []JSONNode
}{
{
name: "simple property",
data: map[string]interface{}{
"name": "John",
"age": 30,
},
path: "$.name",
expected: []JSONNode{
{Value: "John", Path: "$.name"},
},
},
{
name: "nested property",
data: map[string]interface{}{
"user": map[string]interface{}{
"name": "John",
"age": 30,
},
},
path: "$.user.name",
expected: []JSONNode{
{Value: "John", Path: "$.user.name"},
},
},
{
name: "array access",
data: map[string]interface{}{
"users": []interface{}{
map[string]interface{}{"name": "John", "age": 30},
map[string]interface{}{"name": "Jane", "age": 25},
},
},
path: "$.users[1].name",
expected: []JSONNode{
{Value: "Jane", Path: "$.users[1].name"},
},
},
{
name: "wildcard",
data: map[string]interface{}{
"users": []interface{}{
map[string]interface{}{"name": "John", "age": 30},
map[string]interface{}{"name": "Jane", "age": 25},
},
},
path: "$.users[*].name",
expected: []JSONNode{
{Value: "John", Path: "$.users[0].name"},
{Value: "Jane", Path: "$.users[1].name"},
},
},
{
name: "recursive descent",
data: map[string]interface{}{
"user": map[string]interface{}{
"name": "John",
"profile": map[string]interface{}{
"email": "john@example.com",
},
},
"admin": map[string]interface{}{
"email": "admin@example.com",
},
},
path: "$..email",
expected: []JSONNode{
{Value: "john@example.com", Path: "$.user.profile.email"},
{Value: "admin@example.com", Path: "$.admin.email"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Get(tt.data, tt.path)
// Check if lengths match
if len(result) != len(tt.expected) {
t.Errorf("GetWithPaths() returned %d items, expected %d", len(result), len(tt.expected))
return
}
// For each expected item, find its match in the results and verify both value and path
for _, expected := range tt.expected {
found := false
for _, r := range result {
// Check if value matches
if reflect.DeepEqual(r.Value, expected.Value) {
found = true
// Check if path matches
if r.Path != expected.Path {
t.Errorf("Path mismatch for value %v: got %s, expected %s", r.Value, r.Path, expected.Path)
}
break
}
}
if !found {
t.Errorf("Expected node with value %v and path %s not found in results", expected.Value, expected.Path)
}
}
})
}
}

View File

@@ -92,78 +92,90 @@ func TestEvaluator(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
expected []interface{} expected []JSONNode
}{ }{
{ {
name: "simple_property_access", name: "simple_property_access",
path: "$.store.bicycle.color", path: "$.store.bicycle.color",
expected: []interface{}{"red"}, expected: []JSONNode{
{Value: "red", Path: "$.store.bicycle.color"},
},
}, },
{ {
name: "array_index_access", name: "array_index_access",
path: "$.store.book[0].title", path: "$.store.book[0].title",
expected: []interface{}{"The Fellowship of the Ring"}, expected: []JSONNode{
{Value: "The Fellowship of the Ring", Path: "$.store.book[0].title"},
},
}, },
{ {
name: "wildcard_array_access", name: "wildcard_array_access",
path: "$.store.book[*].title", path: "$.store.book[*].title",
expected: []interface{}{ expected: []JSONNode{
"The Fellowship of the Ring", {Value: "The Fellowship of the Ring", Path: "$.store.book[0].title"},
"The Two Towers", {Value: "The Two Towers", Path: "$.store.book[1].title"},
}, },
}, },
{ {
name: "recursive_price_search", name: "recursive_price_search",
path: "$..price", path: "$..price",
expected: []interface{}{ expected: []JSONNode{
22.99, {Value: 22.99, Path: "$.store.book[0].price"},
23.45, {Value: 23.45, Path: "$.store.book[1].price"},
199.95, {Value: 199.95, Path: "$.store.bicycle.price"},
}, },
}, },
{ {
name: "wildcard_recursive", name: "wildcard_recursive",
path: "$..*", path: "$..*",
expected: []interface{}{ expected: []JSONNode{
// testData["store"], // Root element // These will be compared by value only, paths will be validated separately
// Store children {Value: testData["store"].(map[string]interface{})["book"]},
testData["store"].(map[string]interface{})["book"], {Value: testData["store"].(map[string]interface{})["bicycle"]},
testData["store"].(map[string]interface{})["bicycle"], {Value: testData["store"].(map[string]interface{})["book"].([]interface{})[0]},
// Books {Value: testData["store"].(map[string]interface{})["book"].([]interface{})[1]},
testData["store"].(map[string]interface{})["book"].([]interface{})[0], {Value: "The Fellowship of the Ring"},
testData["store"].(map[string]interface{})["book"].([]interface{})[1], {Value: 22.99},
// Book contents {Value: "The Two Towers"},
"The Fellowship of the Ring", {Value: 23.45},
22.99, {Value: "red"},
"The Two Towers", {Value: 199.95},
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", name: "invalid_index",
path: "$.store.book[5]", path: "$.store.book[5]",
expected: []interface{}{}, expected: []JSONNode{},
}, },
{ {
name: "nonexistent_property", name: "nonexistent_property",
path: "$.store.nonexistent", path: "$.store.nonexistent",
expected: []interface{}{}, expected: []JSONNode{},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Use GetWithPaths directly
result := Get(testData, tt.path) result := Get(testData, tt.path)
// Special handling for wildcard recursive test // Special handling for wildcard recursive test
if tt.name == "wildcard_recursive" { if tt.name == "wildcard_recursive" {
if len(result) != len(tt.expected) { // Skip length check for wildcard recursive since it might vary
t.Errorf("Expected %d items, got %d", len(tt.expected), len(result)) // Just verify that each expected item is in the results
// Validate values match and paths are filled in
for _, e := range tt.expected {
found := false
for _, r := range result {
if reflect.DeepEqual(r.Value, e.Value) {
found = true
break
}
}
if !found {
t.Errorf("Expected value %v not found in results", e.Value)
}
} }
return return
} }
@@ -172,14 +184,15 @@ func TestEvaluator(t *testing.T) {
t.Errorf("Expected %d items, got %d", len(tt.expected), len(result)) t.Errorf("Expected %d items, got %d", len(tt.expected), len(result))
} }
expectedSet := make(map[interface{}]bool, len(tt.expected)) // Validate both values and paths
for _, expected := range tt.expected { for i, e := range tt.expected {
expectedSet[expected] = true if i < len(result) {
if !reflect.DeepEqual(result[i].Value, e.Value) {
t.Errorf("Value at [%d]: got %v, expected %v", i, result[i].Value, e.Value)
}
if result[i].Path != e.Path {
t.Errorf("Path at [%d]: got %s, expected %s", i, result[i].Path, e.Path)
} }
for _, resultItem := range result {
if !expectedSet[resultItem] {
t.Errorf("Expected %v, got %v", tt.expected, resultItem)
} }
} }
}) })
@@ -206,8 +219,79 @@ func TestEdgeCases(t *testing.T) {
"42": "answer", "42": "answer",
} }
result := Get(data, "$.42") result := Get(data, "$.42")
if len(result) == 0 || result[0] != "answer" { if len(result) == 0 || result[0].Value != "answer" {
t.Errorf("Expected 'answer', got %v", result) t.Errorf("Expected 'answer', got %v", result)
} }
}) })
} }
func TestGetWithPaths(t *testing.T) {
tests := []struct {
name string
path string
expected []JSONNode
}{
{
name: "simple_property_access",
path: "$.store.bicycle.color",
expected: []JSONNode{
{Value: "red", Path: "$.store.bicycle.color"},
},
},
{
name: "array_index_access",
path: "$.store.book[0].title",
expected: []JSONNode{
{Value: "The Fellowship of the Ring", Path: "$.store.book[0].title"},
},
},
{
name: "wildcard_array_access",
path: "$.store.book[*].title",
expected: []JSONNode{
{Value: "The Fellowship of the Ring", Path: "$.store.book[0].title"},
{Value: "The Two Towers", Path: "$.store.book[1].title"},
},
},
{
name: "recursive_price_search",
path: "$..price",
expected: []JSONNode{
{Value: 22.99, Path: "$.store.book[0].price"},
{Value: 23.45, Path: "$.store.book[1].price"},
{Value: 199.95, Path: "$.store.bicycle.price"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Get(testData, tt.path)
// Check if lengths match
if len(result) != len(tt.expected) {
t.Errorf("GetWithPaths() returned %d items, expected %d", len(result), len(tt.expected))
return
}
// For each expected item, find its match in the results and verify both value and path
for _, expected := range tt.expected {
found := false
for _, r := range result {
// First verify the value matches
if reflect.DeepEqual(r.Value, expected.Value) {
found = true
// Then verify the path matches
if r.Path != expected.Path {
t.Errorf("Path mismatch for value %v: got %s, expected %s", r.Value, r.Path, expected.Path)
}
break
}
}
if !found {
t.Errorf("Expected node with value %v and path %s not found in results", expected.Value, expected.Path)
}
}
})
}
}