Remove jsonpath and xpath
This commit is contained in:
@@ -1,490 +0,0 @@
|
|||||||
package jsonpath
|
|
||||||
|
|
||||||
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 "*")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
const (
|
|
||||||
RootStep StepType = iota // $ - The root element
|
|
||||||
ChildStep // .key - Direct child access
|
|
||||||
RecursiveDescentStep // ..key - Recursive search for key
|
|
||||||
WildcardStep // .* - All children of an object
|
|
||||||
IndexStep // [n] - Array index access (or [*] for all elements)
|
|
||||||
)
|
|
||||||
|
|
||||||
// TraversalMode determines how the traversal behaves
|
|
||||||
type TraversalMode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
CollectMode TraversalMode = iota // Just collect matched nodes
|
|
||||||
ModifyFirstMode // Modify first matching node
|
|
||||||
ModifyAllMode // Modify all matching nodes
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseJSONPath parses a JSONPath string into a sequence of steps
|
|
||||||
func ParseJSONPath(path string) ([]JSONStep, error) {
|
|
||||||
if len(path) == 0 || path[0] != '$' {
|
|
||||||
return nil, fmt.Errorf("path must start with $; received: %q", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
steps := []JSONStep{}
|
|
||||||
i := 0
|
|
||||||
|
|
||||||
for i < len(path) {
|
|
||||||
switch path[i] {
|
|
||||||
case '$':
|
|
||||||
steps = append(steps, JSONStep{Type: RootStep})
|
|
||||||
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; error: %w", indexStr, err)
|
|
||||||
}
|
|
||||||
steps = append(steps, JSONStep{Type: IndexStep, Index: index})
|
|
||||||
}
|
|
||||||
i = nextPos + 1 // Skip closing ]
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected character: %c at position %d; path: %q", path[i], i, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return steps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readKey extracts a key name from the path
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// readIndex extracts an array index or wildcard from the path
|
|
||||||
func readIndex(path string, start int) (string, int) {
|
|
||||||
i := start
|
|
||||||
for ; i < len(path); i++ {
|
|
||||||
if path[i] == ']' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path[start:i], i
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves values with their paths from data at the specified JSONPath
|
|
||||||
// Each returned JSONNode contains both the value and its exact path in the data structure
|
|
||||||
func Get(data interface{}, path string) ([]JSONNode, error) {
|
|
||||||
steps, err := ParseJSONPath(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
results := []JSONNode{}
|
|
||||||
err = traverseWithPaths(data, steps, &results, "$")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to traverse JSONPath %q: %w", path, err)
|
|
||||||
}
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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{}) error {
|
|
||||||
steps, err := ParseJSONPath(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
success := false
|
|
||||||
err = setWithPath(data, steps, &success, value, "$", ModifyFirstMode)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", path, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAll updates all matching values at the specified JSONPath.
|
|
||||||
func SetAll(data interface{}, path string, value interface{}) error {
|
|
||||||
steps, err := ParseJSONPath(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
success := false
|
|
||||||
err = setWithPath(data, steps, &success, value, "$", ModifyAllMode)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", path, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setWithPath modifies values while tracking paths
|
|
||||||
func setWithPath(node interface{}, steps []JSONStep, success *bool, value interface{}, currentPath string, mode TraversalMode) error {
|
|
||||||
if node == nil || *success && mode == ModifyFirstMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip root step
|
|
||||||
actualSteps := steps
|
|
||||||
if len(steps) > 0 && steps[0].Type == RootStep {
|
|
||||||
actualSteps = steps[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have no steps left, we're setting the root value
|
|
||||||
if len(actualSteps) == 0 {
|
|
||||||
// For the root node, we need to handle it differently depending on what's passed in
|
|
||||||
// since we can't directly replace the interface{} variable
|
|
||||||
|
|
||||||
// We'll signal success and let the JSONProcessor handle updating the root
|
|
||||||
*success = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 fmt.Errorf("node at path %q is not a map; actual type: %T", currentPath, node)
|
|
||||||
}
|
|
||||||
|
|
||||||
childPath := currentPath + "." + step.Key
|
|
||||||
|
|
||||||
if isLastStep {
|
|
||||||
// We've reached the target, set the value
|
|
||||||
m[step.Key] = value
|
|
||||||
*success = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create intermediate nodes if necessary
|
|
||||||
child, exists := m[step.Key]
|
|
||||||
if !exists {
|
|
||||||
// Create missing intermediate node
|
|
||||||
if len(remainingSteps) > 0 && remainingSteps[0].Type == IndexStep {
|
|
||||||
child = []interface{}{}
|
|
||||||
} else {
|
|
||||||
child = map[string]interface{}{}
|
|
||||||
}
|
|
||||||
m[step.Key] = child
|
|
||||||
}
|
|
||||||
|
|
||||||
err := setWithPath(child, remainingSteps, success, value, childPath, mode)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", childPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case IndexStep:
|
|
||||||
arr, ok := node.([]interface{})
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("node at path %q is not an array; actual type: %T", currentPath, node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle wildcard index
|
|
||||||
if step.Index == -1 {
|
|
||||||
for i, item := range arr {
|
|
||||||
itemPath := fmt.Sprintf("%s[%d]", currentPath, i)
|
|
||||||
if isLastStep {
|
|
||||||
arr[i] = value
|
|
||||||
*success = true
|
|
||||||
if mode == ModifyFirstMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := setWithPath(item, remainingSteps, success, value, itemPath, mode)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", itemPath, err)
|
|
||||||
}
|
|
||||||
if *success && mode == ModifyFirstMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
arr[step.Index] = value
|
|
||||||
*success = true
|
|
||||||
} else {
|
|
||||||
err := setWithPath(item, remainingSteps, success, value, itemPath, mode)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", itemPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
m[step.Key] = value
|
|
||||||
*success = true
|
|
||||||
if mode == ModifyFirstMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := setWithPath(val, remainingSteps, success, value, directPath, mode)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", directPath, err)
|
|
||||||
}
|
|
||||||
if *success && mode == ModifyFirstMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then continue recursion to all children
|
|
||||||
switch n := node.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
for k, v := range n {
|
|
||||||
childPath := currentPath + "." + k
|
|
||||||
// Skip keys we've already processed directly
|
|
||||||
if step.Key != "*" && k == step.Key {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := setWithPath(v, steps, success, value, childPath, mode)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", childPath, err)
|
|
||||||
}
|
|
||||||
if *success && mode == ModifyFirstMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case []interface{}:
|
|
||||||
for i, v := range n {
|
|
||||||
childPath := fmt.Sprintf("%s[%d]", currentPath, i)
|
|
||||||
err := setWithPath(v, steps, success, value, childPath, mode)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", childPath, err)
|
|
||||||
}
|
|
||||||
if *success && mode == ModifyFirstMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case WildcardStep:
|
|
||||||
m, ok := node.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("node at path %q is not a map; actual type: %T", currentPath, node)
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range m {
|
|
||||||
childPath := currentPath + "." + k
|
|
||||||
if isLastStep {
|
|
||||||
m[k] = value
|
|
||||||
*success = true
|
|
||||||
if mode == ModifyFirstMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := setWithPath(v, remainingSteps, success, value, childPath, mode)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", childPath, err)
|
|
||||||
}
|
|
||||||
if *success && mode == ModifyFirstMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// traverseWithPaths tracks both nodes and their paths during traversal
|
|
||||||
func traverseWithPaths(node interface{}, steps []JSONStep, results *[]JSONNode, currentPath string) error {
|
|
||||||
if len(steps) == 0 || node == nil {
|
|
||||||
return fmt.Errorf("cannot traverse with empty steps or nil node; steps length: %d, node: %v", len(steps), node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip root step
|
|
||||||
actualSteps := steps
|
|
||||||
if steps[0].Type == RootStep {
|
|
||||||
if len(steps) == 1 {
|
|
||||||
*results = append(*results, JSONNode{Value: node, Path: currentPath})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
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 fmt.Errorf("node is not a map; actual type: %T", node)
|
|
||||||
}
|
|
||||||
|
|
||||||
child, exists := m[step.Key]
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("key not found: %s in node at path: %s", step.Key, currentPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
childPath := currentPath + "." + step.Key
|
|
||||||
if isLastStep {
|
|
||||||
*results = append(*results, JSONNode{Value: child, Path: childPath})
|
|
||||||
} else {
|
|
||||||
err := traverseWithPaths(child, remainingSteps, results, childPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", childPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case IndexStep:
|
|
||||||
arr, ok := node.([]interface{})
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("node is not an array; actual type: %T", node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
err := traverseWithPaths(item, remainingSteps, results, itemPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", itemPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
err := traverseWithPaths(item, remainingSteps, results, itemPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", itemPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("index %d out of bounds for array at path: %s", step.Index, currentPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
err := traverseWithPaths(val, remainingSteps, results, directPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", directPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
err := traverseWithPaths(v, steps, results, childPath) // Use the same steps
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", childPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case []interface{}:
|
|
||||||
for i, v := range n {
|
|
||||||
childPath := fmt.Sprintf("%s[%d]", currentPath, i)
|
|
||||||
err := traverseWithPaths(v, steps, results, childPath) // Use the same steps
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", childPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case WildcardStep:
|
|
||||||
m, ok := node.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("node is not a map; actual type: %T", node)
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range m {
|
|
||||||
childPath := currentPath + "." + k
|
|
||||||
if isLastStep {
|
|
||||||
*results = append(*results, JSONNode{Value: v, Path: childPath})
|
|
||||||
} else {
|
|
||||||
err := traverseWithPaths(v, remainingSteps, results, childPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", childPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,577 +0,0 @@
|
|||||||
package jsonpath
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetWithPathsBasic(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
data map[string]interface{}
|
|
||||||
path string
|
|
||||||
expected []JSONNode
|
|
||||||
error bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
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"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nonexistent path",
|
|
||||||
data: map[string]interface{}{
|
|
||||||
"user": map[string]interface{}{
|
|
||||||
"name": "John",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
path: "$.user.email",
|
|
||||||
expected: []JSONNode{},
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result, err := Get(tt.data, tt.path)
|
|
||||||
if err != nil {
|
|
||||||
if !tt.error {
|
|
||||||
t.Errorf("GetWithPaths() returned error: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For nonexistent path, we expect empty slice
|
|
||||||
if tt.name == "nonexistent path" {
|
|
||||||
if len(result) > 0 {
|
|
||||||
t.Errorf("GetWithPaths() returned %v, expected empty result", result)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 wildcard results, we need to check containment rather than exact order
|
|
||||||
if tt.name == "wildcard" || tt.name == "recursive descent" {
|
|
||||||
// For each expected item, check if it exists in the results by both value and path
|
|
||||||
for _, expected := range tt.expected {
|
|
||||||
found := false
|
|
||||||
for _, r := range result {
|
|
||||||
if reflect.DeepEqual(r.Value, expected.Value) && r.Path == expected.Path {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Errorf("GetWithPaths() missing expected value: %v with path: %s", expected.Value, expected.Path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Otherwise check exact equality of both values and paths
|
|
||||||
for i, expected := range 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSet(t *testing.T) {
|
|
||||||
t.Run("simple property", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"name": "John",
|
|
||||||
"age": 30,
|
|
||||||
}
|
|
||||||
err := Set(data, "$.name", "Jane")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if data["name"] != "Jane" {
|
|
||||||
t.Errorf("Set() failed: expected name to be 'Jane', got %v", data["name"])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("nested property", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"user": map[string]interface{}{
|
|
||||||
"name": "John",
|
|
||||||
"age": 30,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := Set(data, "$.user.name", "Jane")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, ok := data["user"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("User is not a map")
|
|
||||||
}
|
|
||||||
if user["name"] != "Jane" {
|
|
||||||
t.Errorf("Set() failed: expected user.name to be 'Jane', got %v", user["name"])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("array element", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"users": []interface{}{
|
|
||||||
map[string]interface{}{"name": "John", "age": 30},
|
|
||||||
map[string]interface{}{"name": "Jane", "age": 25},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := Set(data, "$.users[0].name", "Bob")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
users, ok := data["users"].([]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Users is not a slice")
|
|
||||||
}
|
|
||||||
user0, ok := users[0].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("User is not a map")
|
|
||||||
}
|
|
||||||
if user0["name"] != "Bob" {
|
|
||||||
t.Errorf("Set() failed: expected users[0].name to be 'Bob', got %v", user0["name"])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("complex value", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"user": map[string]interface{}{
|
|
||||||
"name": "John",
|
|
||||||
"profile": map[string]interface{}{
|
|
||||||
"email": "john@example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
newProfile := map[string]interface{}{
|
|
||||||
"email": "john.doe@example.com",
|
|
||||||
"phone": "123-456-7890",
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Set(data, "$.user.profile", newProfile)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userMap, ok := data["user"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("User is not a map")
|
|
||||||
}
|
|
||||||
|
|
||||||
profile, ok := userMap["profile"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Profile is not a map")
|
|
||||||
}
|
|
||||||
|
|
||||||
if profile["email"] != "john.doe@example.com" || profile["phone"] != "123-456-7890" {
|
|
||||||
t.Errorf("Set() failed: expected profile to be updated with new values")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create new property", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"user": map[string]interface{}{
|
|
||||||
"name": "John",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Set(data, "$.user.email", "john@example.com")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userMap, ok := data["user"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("User is not a map")
|
|
||||||
}
|
|
||||||
|
|
||||||
if email, exists := userMap["email"]; !exists || email != "john@example.com" {
|
|
||||||
t.Errorf("Set() failed: expected user.email to be 'john@example.com', got %v", userMap["email"])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create nested properties", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"user": map[string]interface{}{
|
|
||||||
"name": "John",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Set(data, "$.user.contact.email", "john@example.com")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userMap, ok := data["user"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("User is not a map")
|
|
||||||
}
|
|
||||||
|
|
||||||
contact, ok := userMap["contact"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Contact is not a map")
|
|
||||||
}
|
|
||||||
|
|
||||||
if email, exists := contact["email"]; !exists || email != "john@example.com" {
|
|
||||||
t.Errorf("Set() failed: expected user.contact.email to be 'john@example.com', got %v", contact["email"])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create array and element", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"user": map[string]interface{}{
|
|
||||||
"name": "John",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should create an empty addresses array, but won't be able to set index 0
|
|
||||||
// since the array is empty
|
|
||||||
err := Set(data, "$.user.addresses[0].street", "123 Main St")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("multiple targets (should only update first)", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"users": []interface{}{
|
|
||||||
map[string]interface{}{"active": true},
|
|
||||||
map[string]interface{}{"active": true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Set(data, "$.users[*].active", false)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
users, ok := data["users"].([]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Users is not a slice")
|
|
||||||
}
|
|
||||||
|
|
||||||
user0, ok := users[0].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("User0 is not a map")
|
|
||||||
}
|
|
||||||
|
|
||||||
user1, ok := users[1].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("User1 is not a map")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only the first one should be changed
|
|
||||||
if active, exists := user0["active"]; !exists || active != false {
|
|
||||||
t.Errorf("Set() failed: expected users[0].active to be false, got %v", user0["active"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// The second one should remain unchanged
|
|
||||||
if active, exists := user1["active"]; !exists || active != true {
|
|
||||||
t.Errorf("Set() incorrectly modified users[1].active: expected true, got %v", user1["active"])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("setting on root should not fail (anymore)", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"name": "John",
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Set(data, "$", "Jane")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data should be unchanged
|
|
||||||
if data["name"] != "John" {
|
|
||||||
t.Errorf("Data was modified when setting on root")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetAll(t *testing.T) {
|
|
||||||
t.Run("simple property", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"name": "John",
|
|
||||||
"age": 30,
|
|
||||||
}
|
|
||||||
err := SetAll(data, "$.name", "Jane")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("SetAll() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if data["name"] != "Jane" {
|
|
||||||
t.Errorf("SetAll() failed: expected name to be 'Jane', got %v", data["name"])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("all array elements", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"users": []interface{}{
|
|
||||||
map[string]interface{}{"active": true},
|
|
||||||
map[string]interface{}{"active": true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := SetAll(data, "$.users[*].active", false)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("SetAll() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
users, ok := data["users"].([]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Users is not a slice")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both elements should be updated
|
|
||||||
for i, user := range users {
|
|
||||||
userMap, ok := user.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("User%d is not a map", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
if active, exists := userMap["active"]; !exists || active != false {
|
|
||||||
t.Errorf("SetAll() failed: expected users[%d].active to be false, got %v", i, userMap["active"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("recursive descent", func(t *testing.T) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"user": map[string]interface{}{
|
|
||||||
"profile": map[string]interface{}{
|
|
||||||
"active": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"admin": map[string]interface{}{
|
|
||||||
"profile": map[string]interface{}{
|
|
||||||
"active": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := SetAll(data, "$..active", false)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("SetAll() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check user profile
|
|
||||||
userProfile, ok := data["user"].(map[string]interface{})["profile"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Failed to access user.profile")
|
|
||||||
}
|
|
||||||
if active, exists := userProfile["active"]; !exists || active != false {
|
|
||||||
t.Errorf("SetAll() didn't update user.profile.active, got: %v", active)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check admin profile
|
|
||||||
adminProfile, ok := data["admin"].(map[string]interface{})["profile"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Failed to access admin.profile")
|
|
||||||
}
|
|
||||||
if active, exists := adminProfile["active"]; !exists || active != false {
|
|
||||||
t.Errorf("SetAll() didn't update admin.profile.active, got: %v", active)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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, err := Get(tt.data, tt.path)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("GetWithPaths() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,318 +0,0 @@
|
|||||||
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 []JSONNode
|
|
||||||
error bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
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"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard_recursive",
|
|
||||||
path: "$..*",
|
|
||||||
expected: []JSONNode{
|
|
||||||
// These will be compared by value only, paths will be validated separately
|
|
||||||
{Value: testData["store"].(map[string]interface{})["book"]},
|
|
||||||
{Value: testData["store"].(map[string]interface{})["bicycle"]},
|
|
||||||
{Value: testData["store"].(map[string]interface{})["book"].([]interface{})[0]},
|
|
||||||
{Value: testData["store"].(map[string]interface{})["book"].([]interface{})[1]},
|
|
||||||
{Value: "The Fellowship of the Ring"},
|
|
||||||
{Value: 22.99},
|
|
||||||
{Value: "The Two Towers"},
|
|
||||||
{Value: 23.45},
|
|
||||||
{Value: "red"},
|
|
||||||
{Value: 199.95},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid_index",
|
|
||||||
path: "$.store.book[5]",
|
|
||||||
expected: []JSONNode{},
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nonexistent_property",
|
|
||||||
path: "$.store.nonexistent",
|
|
||||||
expected: []JSONNode{},
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Use GetWithPaths directly
|
|
||||||
result, err := Get(testData, tt.path)
|
|
||||||
if err != nil {
|
|
||||||
if !tt.error {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special handling for wildcard recursive test
|
|
||||||
if tt.name == "wildcard_recursive" {
|
|
||||||
// Skip length check for wildcard recursive since it might vary
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(result) != len(tt.expected) {
|
|
||||||
t.Errorf("Expected %d items, got %d", len(tt.expected), len(result))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate both values and paths
|
|
||||||
for i, e := range tt.expected {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEdgeCases(t *testing.T) {
|
|
||||||
t.Run("empty_data", func(t *testing.T) {
|
|
||||||
result, err := Get(nil, "$.a.b")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected error for empty data")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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",
|
|
||||||
}
|
|
||||||
result, err := Get(data, "$.42")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) == 0 || result[0].Value != "answer" {
|
|
||||||
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, err := Get(testData, tt.path)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,4 +0,0 @@
|
|||||||
// The package is now using github.com/antchfx/xmlquery for XPath parsing.
|
|
||||||
// The parsing functionality tests have been removed since we're now
|
|
||||||
// delegating XPath parsing to the xmlquery library.
|
|
||||||
package xpath
|
|
@@ -1,4 +0,0 @@
|
|||||||
// The package is now using github.com/antchfx/xmlquery for XPath parsing.
|
|
||||||
// The parsing functionality tests have been removed since we're now
|
|
||||||
// delegating XPath parsing to the xmlquery library.
|
|
||||||
package xpath
|
|
@@ -1,133 +0,0 @@
|
|||||||
package xpath
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/antchfx/xmlquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get retrieves nodes from XML data using an XPath expression
|
|
||||||
func Get(node *xmlquery.Node, path string) ([]*xmlquery.Node, error) {
|
|
||||||
if node == nil {
|
|
||||||
return nil, errors.New("nil node provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute xpath query directly
|
|
||||||
nodes, err := xmlquery.QueryAll(node, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to execute XPath query: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set updates a single node in the XML data using an XPath expression
|
|
||||||
func Set(node *xmlquery.Node, path string, value interface{}) error {
|
|
||||||
if node == nil {
|
|
||||||
return errors.New("nil node provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the node to update
|
|
||||||
nodes, err := xmlquery.QueryAll(node, path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to execute XPath query: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(nodes) == 0 {
|
|
||||||
return fmt.Errorf("no nodes found for path: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the first matching node
|
|
||||||
updateNodeValue(nodes[0], value)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAll updates all nodes that match the XPath expression
|
|
||||||
func SetAll(node *xmlquery.Node, path string, value interface{}) error {
|
|
||||||
if node == nil {
|
|
||||||
return errors.New("nil node provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all nodes to update
|
|
||||||
nodes, err := xmlquery.QueryAll(node, path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to execute XPath query: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(nodes) == 0 {
|
|
||||||
return fmt.Errorf("no nodes found for path: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update all matching nodes
|
|
||||||
for _, matchNode := range nodes {
|
|
||||||
updateNodeValue(matchNode, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to update a node's value
|
|
||||||
func updateNodeValue(node *xmlquery.Node, value interface{}) {
|
|
||||||
strValue := fmt.Sprintf("%v", value)
|
|
||||||
|
|
||||||
// Handle different node types
|
|
||||||
switch node.Type {
|
|
||||||
case xmlquery.AttributeNode:
|
|
||||||
// For attribute nodes, update the attribute value
|
|
||||||
parent := node.Parent
|
|
||||||
if parent != nil {
|
|
||||||
for i, attr := range parent.Attr {
|
|
||||||
if attr.Name.Local == node.Data {
|
|
||||||
parent.Attr[i].Value = strValue
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case xmlquery.TextNode:
|
|
||||||
// For text nodes, update the text content
|
|
||||||
node.Data = strValue
|
|
||||||
case xmlquery.ElementNode:
|
|
||||||
// For element nodes, clear existing text children and add a new text node
|
|
||||||
// First, remove all existing text children
|
|
||||||
var nonTextChildren []*xmlquery.Node
|
|
||||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
|
||||||
if child.Type != xmlquery.TextNode {
|
|
||||||
nonTextChildren = append(nonTextChildren, child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear all children
|
|
||||||
node.FirstChild = nil
|
|
||||||
node.LastChild = nil
|
|
||||||
|
|
||||||
// Add a new text node
|
|
||||||
textNode := &xmlquery.Node{
|
|
||||||
Type: xmlquery.TextNode,
|
|
||||||
Data: strValue,
|
|
||||||
Parent: node,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the text node as the first child
|
|
||||||
node.FirstChild = textNode
|
|
||||||
node.LastChild = textNode
|
|
||||||
|
|
||||||
// Add back non-text children
|
|
||||||
for _, child := range nonTextChildren {
|
|
||||||
child.Parent = node
|
|
||||||
|
|
||||||
// If this is the first child being added back
|
|
||||||
if node.FirstChild == textNode && node.LastChild == textNode {
|
|
||||||
node.FirstChild.NextSibling = child
|
|
||||||
child.PrevSibling = node.FirstChild
|
|
||||||
node.LastChild = child
|
|
||||||
} else {
|
|
||||||
// Add to the end of the chain
|
|
||||||
node.LastChild.NextSibling = child
|
|
||||||
child.PrevSibling = node.LastChild
|
|
||||||
node.LastChild = child
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,474 +0,0 @@
|
|||||||
package xpath
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/antchfx/xmlquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parse test XML data once at the beginning for use in multiple tests
|
|
||||||
func parseTestXML(t *testing.T, xmlData string) *xmlquery.Node {
|
|
||||||
doc, err := xmlquery.Parse(strings.NewReader(xmlData))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse test XML: %v", err)
|
|
||||||
}
|
|
||||||
return doc
|
|
||||||
}
|
|
||||||
|
|
||||||
// XML test data as a string for our tests
|
|
||||||
var testXML = `
|
|
||||||
<store>
|
|
||||||
<book category="fiction">
|
|
||||||
<title lang="en">The Fellowship of the Ring</title>
|
|
||||||
<author>J.R.R. Tolkien</author>
|
|
||||||
<year>1954</year>
|
|
||||||
<price>22.99</price>
|
|
||||||
</book>
|
|
||||||
<book category="fiction">
|
|
||||||
<title lang="en">The Two Towers</title>
|
|
||||||
<author>J.R.R. Tolkien</author>
|
|
||||||
<year>1954</year>
|
|
||||||
<price>23.45</price>
|
|
||||||
</book>
|
|
||||||
<book category="technical">
|
|
||||||
<title lang="en">Learning XML</title>
|
|
||||||
<author>Erik T. Ray</author>
|
|
||||||
<year>2003</year>
|
|
||||||
<price>39.95</price>
|
|
||||||
</book>
|
|
||||||
<bicycle>
|
|
||||||
<color>red</color>
|
|
||||||
<price>199.95</price>
|
|
||||||
</bicycle>
|
|
||||||
</store>
|
|
||||||
`
|
|
||||||
|
|
||||||
func TestEvaluator(t *testing.T) {
|
|
||||||
// Parse the test XML data once for all test cases
|
|
||||||
doc := parseTestXML(t, testXML)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
error bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple_element_access",
|
|
||||||
path: "/store/bicycle/color",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "recursive_element_access",
|
|
||||||
path: "//price",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard_element_access",
|
|
||||||
path: "/store/book/*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "attribute_exists_predicate",
|
|
||||||
path: "//title[@lang]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "attribute_equals_predicate",
|
|
||||||
path: "//title[@lang='en']",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "value_comparison_predicate",
|
|
||||||
path: "/store/book[price>35.00]/title",
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "last_predicate",
|
|
||||||
path: "/store/book[last()]/title",
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "last_minus_predicate",
|
|
||||||
path: "/store/book[last()-1]/title",
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "position_predicate",
|
|
||||||
path: "/store/book[position()<3]/title",
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid_index",
|
|
||||||
path: "/store/book[10]/title",
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nonexistent_element",
|
|
||||||
path: "/store/nonexistent",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result, err := Get(doc, tt.path)
|
|
||||||
|
|
||||||
// Handle expected errors
|
|
||||||
if tt.error {
|
|
||||||
if err == nil && len(result) == 0 {
|
|
||||||
// If we expected an error but got empty results instead, that's okay
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// If we got an error as expected, that's okay
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
// If we didn't expect an error but got one, that's a test failure
|
|
||||||
t.Errorf("Get(%q) returned unexpected error: %v", tt.path, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special cases where we don't care about exact matches
|
|
||||||
switch tt.name {
|
|
||||||
case "wildcard_element_access":
|
|
||||||
// Just check that we got some elements
|
|
||||||
if len(result) == 0 {
|
|
||||||
t.Errorf("Expected multiple elements for wildcard, got none")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case "attribute_exists_predicate", "attribute_equals_predicate":
|
|
||||||
// Just check that we got some titles
|
|
||||||
if len(result) == 0 {
|
|
||||||
t.Errorf("Expected titles with lang attribute, got none")
|
|
||||||
}
|
|
||||||
// Ensure all are title elements
|
|
||||||
for _, node := range result {
|
|
||||||
if node.Data != "title" {
|
|
||||||
t.Errorf("Expected title elements, got: %s", node.Data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case "nonexistent_element":
|
|
||||||
// Just check that we got empty results
|
|
||||||
if len(result) != 0 {
|
|
||||||
t.Errorf("Expected empty results for nonexistent element, got %d items", len(result))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other cases, just verify we got results
|
|
||||||
if len(result) == 0 {
|
|
||||||
t.Errorf("Expected results for path %s, got none", tt.path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEdgeCases(t *testing.T) {
|
|
||||||
t.Run("nil_node", func(t *testing.T) {
|
|
||||||
result, err := Get(nil, "/store/book")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected error for nil node")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) > 0 {
|
|
||||||
t.Errorf("Expected empty result, got %v", result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid_xml", func(t *testing.T) {
|
|
||||||
invalidXML, err := xmlquery.Parse(strings.NewReader("<invalid>xml"))
|
|
||||||
if err != nil {
|
|
||||||
// If parsing fails, that's expected
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = Get(invalidXML, "/store")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for invalid XML structure")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// For these tests with the simple XML, we expect just one result
|
|
||||||
simpleXML := `<root><book><title lang="en">Test</title></book></root>`
|
|
||||||
doc := parseTestXML(t, simpleXML)
|
|
||||||
|
|
||||||
t.Run("current_node", func(t *testing.T) {
|
|
||||||
result, err := Get(doc, "/root/book/.")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) > 1 {
|
|
||||||
t.Errorf("Expected at most 1 result, got %d", len(result))
|
|
||||||
}
|
|
||||||
if len(result) > 0 {
|
|
||||||
// Verify it's the book node
|
|
||||||
if result[0].Data != "book" {
|
|
||||||
t.Errorf("Expected book node, got %v", result[0].Data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("attributes", func(t *testing.T) {
|
|
||||||
result, err := Get(doc, "/root/book/title/@lang")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) != 1 || result[0].InnerText() != "en" {
|
|
||||||
t.Errorf("Expected 'en', got %v", result[0].InnerText())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetWithPaths(t *testing.T) {
|
|
||||||
// Use a simplified, well-formed XML document
|
|
||||||
simpleXML := `<store>
|
|
||||||
<book category="fiction">
|
|
||||||
<title lang="en">The Book Title</title>
|
|
||||||
<author>Author Name</author>
|
|
||||||
<price>19.99</price>
|
|
||||||
</book>
|
|
||||||
<bicycle>
|
|
||||||
<color>red</color>
|
|
||||||
<price>199.95</price>
|
|
||||||
</bicycle>
|
|
||||||
</store>`
|
|
||||||
|
|
||||||
// Parse the XML for testing
|
|
||||||
doc := parseTestXML(t, simpleXML)
|
|
||||||
|
|
||||||
// Debug: Print the test XML
|
|
||||||
t.Logf("Test XML:\n%s", simpleXML)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
expectedValue string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple_element_access",
|
|
||||||
path: "/store/bicycle/color",
|
|
||||||
expectedValue: "red",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "attribute_access",
|
|
||||||
path: "/store/book/title/@lang",
|
|
||||||
expectedValue: "en",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "recursive_with_attribute",
|
|
||||||
path: "//title[@lang='en']",
|
|
||||||
expectedValue: "The Book Title",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Debug: Print the path we're looking for
|
|
||||||
t.Logf("Looking for path: %s", tt.path)
|
|
||||||
|
|
||||||
result, err := Get(doc, tt.path)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get(%q) returned error: %v", tt.path, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug: Print the results
|
|
||||||
t.Logf("Got %d results", len(result))
|
|
||||||
for i, r := range result {
|
|
||||||
t.Logf("Result %d: Node=%s, Value=%v", i, r.Data, r.InnerText())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that we got results
|
|
||||||
if len(result) == 0 {
|
|
||||||
t.Errorf("Get(%q) returned no results", tt.path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For attribute access test, do more specific checks
|
|
||||||
if tt.name == "attribute_access" {
|
|
||||||
// Check the first result's value matches expected
|
|
||||||
if result[0].InnerText() != tt.expectedValue {
|
|
||||||
t.Errorf("Attribute value: got %v, expected %s", result[0].InnerText(), tt.expectedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For simple element access, check the text content
|
|
||||||
if tt.name == "simple_element_access" {
|
|
||||||
if text := result[0].InnerText(); text != tt.expectedValue {
|
|
||||||
t.Errorf("Element text: got %s, expected %s", text, tt.expectedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For recursive with attribute test, check title elements with lang="en"
|
|
||||||
if tt.name == "recursive_with_attribute" {
|
|
||||||
for _, node := range result {
|
|
||||||
// Check the node is a title
|
|
||||||
if node.Data != "title" {
|
|
||||||
t.Errorf("Expected title element, got %s", node.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check text content
|
|
||||||
if text := node.InnerText(); text != tt.expectedValue {
|
|
||||||
t.Errorf("Text content: got %s, expected %s", text, tt.expectedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check attributes - find the lang attribute
|
|
||||||
hasLang := false
|
|
||||||
for _, attr := range node.Attr {
|
|
||||||
if attr.Name.Local == "lang" && attr.Value == "en" {
|
|
||||||
hasLang = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasLang {
|
|
||||||
t.Errorf("Expected lang=\"en\" attribute, but it was not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSet(t *testing.T) {
|
|
||||||
t.Run("simple element", func(t *testing.T) {
|
|
||||||
xmlData := `<root><name>John</name></root>`
|
|
||||||
doc := parseTestXML(t, xmlData)
|
|
||||||
|
|
||||||
err := Set(doc, "/root/name", "Jane")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the change
|
|
||||||
result, err := Get(doc, "/root/name")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) != 1 {
|
|
||||||
t.Errorf("Expected 1 result, got %d", len(result))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check text content
|
|
||||||
if text := result[0].InnerText(); text != "Jane" {
|
|
||||||
t.Errorf("Expected text 'Jane', got '%s'", text)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("attribute", func(t *testing.T) {
|
|
||||||
xmlData := `<root><element id="123"></element></root>`
|
|
||||||
doc := parseTestXML(t, xmlData)
|
|
||||||
|
|
||||||
err := Set(doc, "/root/element/@id", "456")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the change
|
|
||||||
result, err := Get(doc, "/root/element/@id")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) != 1 {
|
|
||||||
t.Errorf("Expected 1 result, got %d", len(result))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For attributes, check the inner text
|
|
||||||
if text := result[0].InnerText(); text != "456" {
|
|
||||||
t.Errorf("Expected attribute value '456', got '%s'", text)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("indexed element", func(t *testing.T) {
|
|
||||||
xmlData := `<root><items><item>first</item><item>second</item></items></root>`
|
|
||||||
doc := parseTestXML(t, xmlData)
|
|
||||||
|
|
||||||
err := Set(doc, "/root/items/item[1]", "changed")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the change using XPath that specifically targets the first item
|
|
||||||
result, err := Get(doc, "/root/items/item[1]")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have results
|
|
||||||
if len(result) == 0 {
|
|
||||||
t.Errorf("Expected at least one result for /root/items/item[1]")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check text content
|
|
||||||
if text := result[0].InnerText(); text != "changed" {
|
|
||||||
t.Errorf("Expected text 'changed', got '%s'", text)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetAll(t *testing.T) {
|
|
||||||
t.Run("multiple elements", func(t *testing.T) {
|
|
||||||
xmlData := `<root><items><item>first</item><item>second</item></items></root>`
|
|
||||||
doc := parseTestXML(t, xmlData)
|
|
||||||
|
|
||||||
err := SetAll(doc, "//item", "changed")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("SetAll() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify all items are changed
|
|
||||||
result, err := Get(doc, "//item")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) != 2 {
|
|
||||||
t.Errorf("Expected 2 results, got %d", len(result))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check each node
|
|
||||||
for i, node := range result {
|
|
||||||
if text := node.InnerText(); text != "changed" {
|
|
||||||
t.Errorf("Item %d: expected text 'changed', got '%s'", i, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("attributes", func(t *testing.T) {
|
|
||||||
xmlData := `<root><item id="1"/><item id="2"/></root>`
|
|
||||||
doc := parseTestXML(t, xmlData)
|
|
||||||
|
|
||||||
err := SetAll(doc, "//item/@id", "new")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("SetAll() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify all attributes are changed
|
|
||||||
result, err := Get(doc, "//item/@id")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) != 2 {
|
|
||||||
t.Errorf("Expected 2 results, got %d", len(result))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For attributes, check inner text
|
|
||||||
for i, node := range result {
|
|
||||||
if text := node.InnerText(); text != "new" {
|
|
||||||
t.Errorf("Attribute %d: expected value 'new', got '%s'", i, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
Reference in New Issue
Block a user