6 Commits

Author SHA1 Message Date
4eed05c7c2 Fix some more minor bugs and tests 2025-03-25 19:14:21 +01:00
4640281fbf Enable root modifications
Though I can not see why you would want to.....
But there's no reason you would not be able to
2025-03-25 18:57:32 +01:00
aba10267d1 Fix more tests 2025-03-25 18:47:55 +01:00
fed140254b Fix some json tests 2025-03-25 18:32:51 +01:00
db92033642 Rework rounding and building lua script
To allow user script to specify what was modified where
2025-03-25 18:28:33 +01:00
1b0b198297 Add xpath tests 2025-03-25 17:56:48 +01:00
7 changed files with 954 additions and 149 deletions

View File

@@ -7,7 +7,6 @@ import (
"modify/processor/jsonpath" "modify/processor/jsonpath"
"os" "os"
"path/filepath" "path/filepath"
"strings"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
) )
@@ -68,6 +67,7 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
return content, 0, 0, nil return content, 0, 0, nil
} }
modCount := 0
for _, node := range nodes { for _, node := range nodes {
log.Printf("Processing node at path: %s with value: %v", node.Path, node.Value) log.Printf("Processing node at path: %s with value: %v", node.Path, node.Value)
@@ -85,10 +85,14 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
} }
log.Printf("Converted node value to Lua: %v", node.Value) log.Printf("Converted node value to Lua: %v", node.Value)
originalScript := luaExpr
fullScript := BuildLuaScript(luaExpr)
log.Printf("Original script: %q, Full script: %q", originalScript, fullScript)
// Execute Lua script // Execute Lua script
log.Printf("Executing Lua script: %s", luaExpr) log.Printf("Executing Lua script: %q", fullScript)
if err := L.DoString(luaExpr); err != nil { if err := L.DoString(fullScript); err != nil {
return content, len(nodes), 0, fmt.Errorf("error executing Lua %s: %v", luaExpr, err) return content, len(nodes), 0, fmt.Errorf("error executing Lua %q: %v", fullScript, err)
} }
log.Println("Lua script executed successfully.") log.Println("Lua script executed successfully.")
@@ -99,51 +103,29 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
} }
log.Printf("Retrieved modified value from Lua: %v", result) log.Printf("Retrieved modified value from Lua: %v", result)
modified := false
modified = L.GetGlobal("modified").String() == "true"
if !modified {
log.Printf("No changes made to node at path: %s", node.Path)
continue
}
// Apply the modification to the JSON data // Apply the modification to the JSON data
err = p.updateJSONValue(jsonData, node.Path, result) err = p.updateJSONValue(jsonData, node.Path, result)
if err != nil { if err != nil {
return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err) return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err)
} }
log.Printf("Updated JSON at path: %s with new value: %v", node.Path, result) log.Printf("Updated JSON at path: %s with new value: %v", node.Path, result)
modCount++
} }
// Convert the modified JSON back to a string with same formatting // Convert the modified JSON back to a string with same formatting
var jsonBytes []byte var jsonBytes []byte
if indent, err := detectJsonIndentation(content); err == nil && indent != "" { jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
// Use detected indentation for output formatting if err != nil {
jsonBytes, err = json.MarshalIndent(jsonData, "", indent) return content, modCount, matchCount, fmt.Errorf("error marshalling JSON: %v", err)
} else {
// Fall back to standard 2-space indent
jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
} }
return string(jsonBytes), modCount, matchCount, nil
// We changed all the nodes trust me bro
return string(jsonBytes), len(nodes), len(nodes), nil
}
// detectJsonIndentation tries to determine the indentation used in the original JSON
func detectJsonIndentation(content string) (string, error) {
lines := strings.Split(content, "\n")
if len(lines) < 2 {
return "", fmt.Errorf("not enough lines to detect indentation")
}
// Look for the first indented line
for i := 1; i < len(lines); i++ {
line := lines[i]
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
// Calculate leading whitespace
indent := line[:len(line)-len(trimmed)]
if len(indent) > 0 {
return indent, nil
}
}
return "", fmt.Errorf("no indentation detected")
} }
// / Selects from the root node // / Selects from the root node
@@ -165,6 +147,52 @@ func detectJsonIndentation(content string) (string, error) {
// updateJSONValue updates a value in the JSON structure based on its JSONPath // updateJSONValue updates a value in the JSON structure based on its JSONPath
func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error { func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error {
// Special handling for root node
if path == "$" {
// For the root node, we'll copy the value to the jsonData reference
// This is a special case since we can't directly replace the interface{} variable
// We need to handle different types of root elements
switch rootValue := newValue.(type) {
case map[string]interface{}:
// For objects, we need to copy over all keys
rootMap, ok := jsonData.(map[string]interface{})
if !ok {
// If the original wasn't a map, completely replace it with the new map
// This is handled by the jsonpath.Set function
return jsonpath.Set(jsonData, path, newValue)
}
// Clear the original map
for k := range rootMap {
delete(rootMap, k)
}
// Copy all keys from the new map
for k, v := range rootValue {
rootMap[k] = v
}
return nil
case []interface{}:
// For arrays, we need to handle similarly
rootArray, ok := jsonData.([]interface{})
if !ok {
// If the original wasn't an array, use jsonpath.Set
return jsonpath.Set(jsonData, path, newValue)
}
// Clear and recreate the array
*&rootArray = rootValue
return nil
default:
// For other types, use jsonpath.Set
return jsonpath.Set(jsonData, path, newValue)
}
}
// For non-root paths, use the regular Set method
err := jsonpath.Set(jsonData, path, newValue) err := jsonpath.Set(jsonData, path, newValue)
if err != nil { if err != nil {
return fmt.Errorf("failed to update JSON value at path '%s': %w", path, err) return fmt.Errorf("failed to update JSON value at path '%s': %w", path, err)

View File

@@ -133,24 +133,24 @@ func TestJSONProcessor_Process_StringValues(t *testing.T) {
t.Logf("Result: %s", result) t.Logf("Result: %s", result)
t.Logf("Match count: %d, Mod count: %d", matchCount, modCount) t.Logf("Match count: %d, Mod count: %d", matchCount, modCount)
if matchCount != 3 { if matchCount != 1 {
t.Errorf("Expected 3 matches, got %d", matchCount) t.Errorf("Expected 1 matches, got %d", matchCount)
} }
if modCount != 3 { if modCount != 1 {
t.Errorf("Expected 3 modifications, got %d", modCount) t.Errorf("Expected 1 modifications, got %d", modCount)
} }
// Check that all expected values are in the result // Check that all expected values are in the result
if !strings.Contains(result, `"maxItems": "200"`) { if !strings.Contains(result, `"maxItems": 200`) {
t.Errorf("Result missing expected value: maxItems=200") t.Errorf("Result missing expected value: maxItems=200")
} }
if !strings.Contains(result, `"itemTimeoutSecs": "60"`) { if !strings.Contains(result, `"itemTimeoutSecs": 60`) {
t.Errorf("Result missing expected value: itemTimeoutSecs=60") t.Errorf("Result missing expected value: itemTimeoutSecs=60")
} }
if !strings.Contains(result, `"retryCount": "10"`) { if !strings.Contains(result, `"retryCount": 10`) {
t.Errorf("Result missing expected value: retryCount=10") t.Errorf("Result missing expected value: retryCount=10")
} }
} }
@@ -265,13 +265,13 @@ func TestJSONProcessor_NestedModifications(t *testing.T) {
"book": [ "book": [
{ {
"category": "reference", "category": "reference",
"title": "Learn Go in 24 Hours", "price": 13.188,
"price": 13.188 "title": "Learn Go in 24 Hours"
}, },
{ {
"category": "fiction", "category": "fiction",
"title": "The Go Developer", "price": 10.788,
"price": 10.788 "title": "The Go Developer"
} }
] ]
} }
@@ -373,20 +373,20 @@ func TestJSONProcessor_ComplexScript(t *testing.T) {
expected := `{ expected := `{
"products": [ "products": [
{ {
"discount": 0.1,
"name": "Basic Widget", "name": "Basic Widget",
"price": 8.991, "price": 8.991
"discount": 0.1
}, },
{ {
"discount": 0.05,
"name": "Premium Widget", "name": "Premium Widget",
"price": 18.9905, "price": 18.9905
"discount": 0.05
} }
] ]
}` }`
p := &JSONProcessor{} p := &JSONProcessor{}
result, modCount, matchCount, err := p.ProcessContent(content, "$.products[*]", "v.price = v.price * (1 - v.discount)") result, modCount, matchCount, err := p.ProcessContent(content, "$.products[*]", "v.price = round(v.price * (1 - v.discount), 4)")
if err != nil { if err != nil {
t.Fatalf("Error processing content: %v", err) t.Fatalf("Error processing content: %v", err)
@@ -419,13 +419,26 @@ func TestJSONProcessor_SpecificItemUpdate(t *testing.T) {
] ]
}` }`
expected := `{ expected := `
{
"items": [ "items": [
{"id": 1, "name": "Item 1", "stock": 10}, {
{"id": 2, "name": "Item 2", "stock": 15}, "id": 1,
{"id": 3, "name": "Item 3", "stock": 0} "name": "Item 1",
"stock": 10
},
{
"id": 2,
"name": "Item 2",
"stock": 15
},
{
"id": 3,
"name": "Item 3",
"stock": 0
}
] ]
}` } `
p := &JSONProcessor{} p := &JSONProcessor{}
result, modCount, matchCount, err := p.ProcessContent(content, "$.items[1].stock", "v=v+10") result, modCount, matchCount, err := p.ProcessContent(content, "$.items[1].stock", "v=v+10")
@@ -454,7 +467,9 @@ func TestJSONProcessor_SpecificItemUpdate(t *testing.T) {
// TestJSONProcessor_RootElementUpdate tests updating the root element // TestJSONProcessor_RootElementUpdate tests updating the root element
func TestJSONProcessor_RootElementUpdate(t *testing.T) { func TestJSONProcessor_RootElementUpdate(t *testing.T) {
content := `{"value": 100}` content := `{"value": 100}`
expected := `{"value": 200}` expected := `{
"value": 200
}`
p := &JSONProcessor{} p := &JSONProcessor{}
result, modCount, matchCount, err := p.ProcessContent(content, "$.value", "v=v*2") result, modCount, matchCount, err := p.ProcessContent(content, "$.value", "v=v*2")
@@ -471,7 +486,11 @@ func TestJSONProcessor_RootElementUpdate(t *testing.T) {
t.Errorf("Expected 1 modification, got %d", modCount) t.Errorf("Expected 1 modification, got %d", modCount)
} }
if result != expected { // Normalize whitespace for comparison
normalizedResult := normalizeWhitespace(result)
normalizedExpected := normalizeWhitespace(expected)
if normalizedResult != normalizedExpected {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
} }
} }
@@ -487,9 +506,9 @@ func TestJSONProcessor_AddNewField(t *testing.T) {
expected := `{ expected := `{
"user": { "user": {
"name": "John",
"age": 30, "age": 30,
"email": "john@example.com" "email": "john@example.com",
"name": "John"
} }
}` }`
@@ -529,8 +548,8 @@ func TestJSONProcessor_RemoveField(t *testing.T) {
expected := `{ expected := `{
"user": { "user": {
"name": "John", "email": "john@example.com",
"email": "john@example.com" "name": "John"
} }
}` }`
@@ -564,8 +583,13 @@ func TestJSONProcessor_ArrayManipulation(t *testing.T) {
"tags": ["go", "json", "lua"] "tags": ["go", "json", "lua"]
}` }`
expected := `{ expected := ` {
"tags": ["GO", "JSON", "LUA", "testing"] "tags": [
"GO",
"JSON",
"LUA",
"testing"
]
}` }`
p := &JSONProcessor{} p := &JSONProcessor{}
@@ -624,28 +648,29 @@ func TestJSONProcessor_ConditionalModification(t *testing.T) {
expected := `{ expected := `{
"products": [ "products": [
{ {
"inStock": true,
"name": "Product A", "name": "Product A",
"price": 9.891, "price": 9.891
"inStock": true
}, },
{ {
"inStock": false,
"name": "Product B", "name": "Product B",
"price": 5.99, "price": 5.99
"inStock": false
}, },
{ {
"inStock": true,
"name": "Product C", "name": "Product C",
"price": 14.391, "price": 14.391
"inStock": true
} }
] ]
}` }`
p := &JSONProcessor{} p := &JSONProcessor{}
result, modCount, matchCount, err := p.ProcessContent(content, "$.products[*]", ` result, modCount, matchCount, err := p.ProcessContent(content, "$.products[*]", `
if v.inStock then if not v.inStock then
v.price = v.price * 0.9 return false
end end
v.price = v.price * 0.9
`) `)
if err != nil { if err != nil {
@@ -695,15 +720,15 @@ func TestJSONProcessor_DeepNesting(t *testing.T) {
"departments": { "departments": {
"engineering": { "engineering": {
"teams": { "teams": {
"frontend": {
"members": 12,
"projects": 5,
"status": "active"
},
"backend": { "backend": {
"members": 8, "members": 8,
"projects": 3, "projects": 3,
"status": "active" "status": "active"
},
"frontend": {
"members": 12,
"projects": 5,
"status": "active"
} }
} }
} }
@@ -763,31 +788,31 @@ func TestJSONProcessor_ComplexTransformation(t *testing.T) {
expected := `{ expected := `{
"order": { "order": {
"items": [
{
"product": "Widget A",
"quantity": 5,
"price": 10.0,
"total": 50.0,
"discounted_total": 45.0
},
{
"product": "Widget B",
"quantity": 3,
"price": 15.0,
"total": 45.0,
"discounted_total": 40.5
}
],
"customer": { "customer": {
"name": "John Smith", "name": "John Smith",
"tier": "gold" "tier": "gold"
}, },
"items": [
{
"discounted_total": 45,
"price": 10,
"product": "Widget A",
"quantity": 5,
"total": 50
},
{
"discounted_total": 40.5,
"price": 15,
"product": "Widget B",
"quantity": 3,
"total": 45
}
],
"summary": { "summary": {
"total_items": 8,
"subtotal": 95.0,
"discount": 9.5, "discount": 9.5,
"total": 85.5 "subtotal": 95,
"total": 85.5,
"total_items": 8
} }
} }
}` }`
@@ -798,19 +823,19 @@ func TestJSONProcessor_ComplexTransformation(t *testing.T) {
local discount_rate = 0.1 -- 10% discount for gold tier local discount_rate = 0.1 -- 10% discount for gold tier
local subtotal = 0 local subtotal = 0
local total_items = 0 local total_items = 0
for i, item in ipairs(v.items) do for i, item in ipairs(v.items) do
-- Calculate item total -- Calculate item total
item.total = item.quantity * item.price item.total = item.quantity * item.price
-- Apply discount -- Apply discount
item.discounted_total = item.total * (1 - discount_rate) item.discounted_total = item.total * (1 - discount_rate)
-- Add to running totals -- Add to running totals
subtotal = subtotal + item.total subtotal = subtotal + item.total
total_items = total_items + item.quantity total_items = total_items + item.quantity
end end
-- Add order summary -- Add order summary
v.summary = { v.summary = {
total_items = total_items, total_items = total_items,
@@ -912,16 +937,16 @@ func TestJSONProcessor_RestructuringData(t *testing.T) {
"people": { "people": {
"developers": [ "developers": [
{ {
"age": 25,
"id": 1, "id": 1,
"name": "Alice", "name": "Alice"
"age": 25
} }
], ],
"managers": [ "managers": [
{ {
"age": 30,
"id": 2, "id": 2,
"name": "Bob", "name": "Bob"
"age": 30
} }
] ]
} }
@@ -935,7 +960,7 @@ func TestJSONProcessor_RestructuringData(t *testing.T) {
developers = {}, developers = {},
managers = {} managers = {}
} }
for _, person in ipairs(old_people) do for _, person in ipairs(old_people) do
local role = person.attributes.role local role = person.attributes.role
local new_person = { local new_person = {
@@ -943,14 +968,14 @@ func TestJSONProcessor_RestructuringData(t *testing.T) {
name = person.name, name = person.name,
age = person.attributes.age age = person.attributes.age
} }
if role == "developer" then if role == "developer" then
table.insert(new_structure.developers, new_person) table.insert(new_structure.developers, new_person)
elseif role == "manager" then elseif role == "manager" then
table.insert(new_structure.managers, new_person) table.insert(new_structure.managers, new_person)
end end
end end
v.people = new_structure v.people = new_structure
`) `)
@@ -982,7 +1007,13 @@ func TestJSONProcessor_FilteringArrayElements(t *testing.T) {
}` }`
expected := `{ expected := `{
"numbers": [2, 4, 6, 8, 10] "numbers": [
2,
4,
6,
8,
10
]
}` }`
p := &JSONProcessor{} p := &JSONProcessor{}
@@ -1017,3 +1048,51 @@ func TestJSONProcessor_FilteringArrayElements(t *testing.T) {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
} }
} }
// TestJSONProcessor_RootNodeModification tests modifying the root node directly
func TestJSONProcessor_RootNodeModification(t *testing.T) {
content := `{
"name": "original",
"value": 100
}`
expected := `{
"description": "This is a completely modified root",
"name": "modified",
"values": [
1,
2,
3
]
}`
p := &JSONProcessor{}
result, modCount, matchCount, err := p.ProcessContent(content, "$", `
-- Completely replace the root node
v = {
name = "modified",
description = "This is a completely modified root",
values = {1, 2, 3}
}
`)
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
if matchCount != 1 {
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
}
// Normalize whitespace for comparison
normalizedResult := normalizeWhitespace(result)
normalizedExpected := normalizeWhitespace(expected)
if normalizedResult != normalizedExpected {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
}
}

View File

@@ -138,10 +138,6 @@ func Set(data interface{}, path string, value interface{}) error {
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err) return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
} }
if len(steps) <= 1 {
return fmt.Errorf("cannot set root node; the provided path %q is invalid", path)
}
success := false success := false
err = setWithPath(data, steps, &success, value, "$", ModifyFirstMode) err = setWithPath(data, steps, &success, value, "$", ModifyFirstMode)
if err != nil { if err != nil {
@@ -157,10 +153,6 @@ func SetAll(data interface{}, path string, value interface{}) error {
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err) return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
} }
if len(steps) <= 1 {
return fmt.Errorf("cannot set root node; the provided path %q is invalid", path)
}
success := false success := false
err = setWithPath(data, steps, &success, value, "$", ModifyAllMode) err = setWithPath(data, steps, &success, value, "$", ModifyAllMode)
if err != nil { if err != nil {
@@ -178,17 +170,20 @@ func setWithPath(node interface{}, steps []JSONStep, success *bool, value interf
// Skip root step // Skip root step
actualSteps := steps actualSteps := steps
if len(steps) > 0 && steps[0].Type == RootStep { if len(steps) > 0 && steps[0].Type == RootStep {
if len(steps) == 1 {
return fmt.Errorf("cannot set root node; the provided path %q is invalid", currentPath)
}
actualSteps = steps[1:] actualSteps = steps[1:]
} }
// Process the first step // If we have no steps left, we're setting the root value
if len(actualSteps) == 0 { if len(actualSteps) == 0 {
return fmt.Errorf("cannot set root node; no steps provided for path %q", currentPath) // 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] step := actualSteps[0]
remainingSteps := actualSteps[1:] remainingSteps := actualSteps[1:]
isLastStep := len(remainingSteps) == 0 isLastStep := len(remainingSteps) == 0

View File

@@ -2,7 +2,6 @@ package processor
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
@@ -81,6 +80,8 @@ func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
return lua.LBool(v), nil return lua.LBool(v), nil
case float64: case float64:
return lua.LNumber(v), nil return lua.LNumber(v), nil
case nil:
return lua.LNil, nil
default: default:
return nil, fmt.Errorf("unsupported data type: %T", data) return nil, fmt.Errorf("unsupported data type: %T", data)
} }
@@ -89,29 +90,31 @@ func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
// FromLua converts a Lua table to a struct or map recursively // FromLua converts a Lua table to a struct or map recursively
func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) { func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) {
switch v := luaValue.(type) { switch v := luaValue.(type) {
// Well shit...
// Tables in lua are both maps and arrays
// As arrays they are ordered and as maps, obviously, not
// So when we parse them to a go map we fuck up the order for arrays
// We have to find a better way....
case *lua.LTable: case *lua.LTable:
result := make(map[string]interface{}) isArray, err := IsLuaTableArray(L, v)
v.ForEach(func(key lua.LValue, value lua.LValue) { if err != nil {
result[key.String()], _ = FromLua(L, value) return nil, err
})
// This may be a bit wasteful...
// Hopefully it won't run often enough to matter
isArray := true
for key := range result {
_, err := strconv.Atoi(key)
if err != nil {
isArray = false
break
}
} }
if isArray { if isArray {
list := make([]interface{}, 0, len(result)) result := make([]interface{}, 0)
for _, value := range result { v.ForEach(func(key lua.LValue, value lua.LValue) {
list = append(list, value) converted, _ := FromLua(L, value)
} result = append(result, converted)
return list, nil })
return result, nil
} else {
result := make(map[string]interface{})
v.ForEach(func(key lua.LValue, value lua.LValue) {
converted, _ := FromLua(L, value)
result[key.String()] = converted
})
return result, nil
} }
return result, nil
case lua.LString: case lua.LString:
return string(v), nil return string(v), nil
case lua.LBool: case lua.LBool:
@@ -123,13 +126,34 @@ func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) {
} }
} }
func IsLuaTableArray(L *lua.LState, v *lua.LTable) (bool, error) {
L.SetGlobal("table_to_check", v)
// Use our predefined helper function from InitLuaHelpers
err := L.DoString(`is_array = isArray(table_to_check)`)
if err != nil {
return false, fmt.Errorf("error determining if table is array: %w", err)
}
// Check the result of our Lua function
isArray := L.GetGlobal("is_array")
// LVIsFalse returns true if a given LValue is a nil or false otherwise false.
if !lua.LVIsFalse(isArray) {
return true, nil
}
return false, nil
}
// InitLuaHelpers initializes common Lua helper functions // InitLuaHelpers initializes common Lua helper functions
func InitLuaHelpers(L *lua.LState) error { func InitLuaHelpers(L *lua.LState) error {
helperScript := ` helperScript := `
-- Custom Lua helpers for math operations -- Custom Lua helpers for math operations
function min(a, b) return math.min(a, b) end function min(a, b) return math.min(a, b) end
function max(a, b) return math.max(a, b) end function max(a, b) return math.max(a, b) end
function round(x) return math.floor(x + 0.5) end function round(x, n)
if n == nil then n = 0 end
return math.floor(x * 10^n + 0.5) / 10^n
end
function floor(x) return math.floor(x) end function floor(x) return math.floor(x) end
function ceil(x) return math.ceil(x) end function ceil(x) return math.ceil(x) end
function upper(s) return string.upper(s) end function upper(s) return string.upper(s) end
@@ -149,6 +173,22 @@ end
function is_number(str) function is_number(str)
return tonumber(str) ~= nil return tonumber(str) ~= nil
end end
function isArray(t)
if type(t) ~= "table" then return false end
local max = 0
local count = 0
for k, _ in pairs(t) do
if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then
return false
end
max = math.max(max, k)
count = count + 1
end
return max == count
end
modified = false
` `
if err := L.DoString(helperScript); err != nil { if err := L.DoString(helperScript); err != nil {
return fmt.Errorf("error loading helper functions: %v", err) return fmt.Errorf("error loading helper functions: %v", err)
@@ -167,8 +207,7 @@ func LimitString(s string, maxLen int) string {
return s[:maxLen-3] + "..." return s[:maxLen-3] + "..."
} }
// BuildLuaScript prepares a Lua expression from shorthand notation func PrependLuaAssignment(luaExpr string) string {
func BuildLuaScript(luaExpr string) string {
// Auto-prepend v1 for expressions starting with operators // Auto-prepend v1 for expressions starting with operators
if strings.HasPrefix(luaExpr, "*") || if strings.HasPrefix(luaExpr, "*") ||
strings.HasPrefix(luaExpr, "/") || strings.HasPrefix(luaExpr, "/") ||
@@ -186,10 +225,30 @@ func BuildLuaScript(luaExpr string) string {
if !strings.Contains(luaExpr, "=") { if !strings.Contains(luaExpr, "=") {
luaExpr = "v1 = " + luaExpr luaExpr = "v1 = " + luaExpr
} }
return luaExpr return luaExpr
} }
// BuildLuaScript prepares a Lua expression from shorthand notation
func BuildLuaScript(luaExpr string) string {
luaExpr = PrependLuaAssignment(luaExpr)
// This allows the user to specify whether or not they modified a value
// If they do nothing we assume they did modify (no return at all)
// If they return before our return then they themselves specify what they did
// If nothing is returned lua assumes nil
// So we can say our value was modified if the return value is either nil or true
// If the return value is false then the user wants to keep the original
fullScript := fmt.Sprintf(`
function run()
%s
end
local res = run()
modified = res == nil or res
`, luaExpr)
return fullScript
}
// Max returns the maximum of two integers // Max returns the maximum of two integers
func Max(a, b int) int { func Max(a, b int) int {
if a > b { if a > b {

View File

@@ -35,10 +35,11 @@ func TestBuildLuaScript(t *testing.T) {
{"v1 * 2", "v1 = v1 * 2"}, {"v1 * 2", "v1 = v1 * 2"},
{"v1 * v2", "v1 = v1 * v2"}, {"v1 * v2", "v1 = v1 * v2"},
{"v1 / v2", "v1 = v1 / v2"}, {"v1 / v2", "v1 = v1 / v2"},
{"12", "v1 = 12"},
} }
for _, c := range cases { for _, c := range cases {
result := BuildLuaScript(c.input) result := PrependLuaAssignment(c.input)
if result != c.expected { if result != c.expected {
t.Errorf("BuildLuaScript(%q): expected %q, got %q", c.input, c.expected, result) t.Errorf("BuildLuaScript(%q): expected %q, got %q", c.input, c.expected, result)
} }

98
processor/xpath/xpath.go Normal file
View File

@@ -0,0 +1,98 @@
package xpath
import "errors"
// XPathStep represents a single step in an XPath expression
type XPathStep struct {
Type StepType
Name string
Predicate *Predicate
}
// StepType defines the type of XPath step
type StepType int
const (
// RootStep represents the root step (/)
RootStep StepType = iota
// ChildStep represents a child element step (element)
ChildStep
// RecursiveDescentStep represents a recursive descent step (//)
RecursiveDescentStep
// WildcardStep represents a wildcard step (*)
WildcardStep
// PredicateStep represents a predicate condition step ([...])
PredicateStep
)
// PredicateType defines the type of XPath predicate
type PredicateType int
const (
// IndexPredicate represents an index predicate [n]
IndexPredicate PredicateType = iota
// LastPredicate represents a last() function predicate
LastPredicate
// LastMinusPredicate represents a last()-n predicate
LastMinusPredicate
// PositionPredicate represents position()-based predicates
PositionPredicate
// AttributeExistsPredicate represents [@attr] predicate
AttributeExistsPredicate
// AttributeEqualsPredicate represents [@attr='value'] predicate
AttributeEqualsPredicate
// ComparisonPredicate represents element comparison predicates
ComparisonPredicate
)
// Predicate represents a condition in XPath
type Predicate struct {
Type PredicateType
Index int
Offset int
Attribute string
Value string
Expression string
}
// XMLNode represents a node in the result set with its value and path
type XMLNode struct {
Value interface{}
Path string
}
// ParseXPath parses an XPath expression into a series of steps
func ParseXPath(path string) ([]XPathStep, error) {
if path == "" {
return nil, errors.New("empty path")
}
// This is just a placeholder implementation for the tests
// The actual implementation would parse the XPath expression
return nil, errors.New("not implemented")
}
// Get retrieves nodes from XML data using an XPath expression
func Get(data interface{}, path string) ([]XMLNode, error) {
if data == "" {
return nil, errors.New("empty XML data")
}
// This is just a placeholder implementation for the tests
// The actual implementation would evaluate the XPath against the XML
return nil, errors.New("not implemented")
}
// Set updates a node in the XML data using an XPath expression
func Set(xmlData string, path string, value interface{}) (string, error) {
// This is just a placeholder implementation for the tests
// The actual implementation would modify the XML based on the XPath
return "", errors.New("not implemented")
}
// SetAll updates all nodes matching an XPath expression in the XML data
func SetAll(xmlData string, path string, value interface{}) (string, error) {
// This is just a placeholder implementation for the tests
// The actual implementation would modify all matching nodes
return "", errors.New("not implemented")
}

View File

@@ -0,0 +1,545 @@
package xpath
import (
"reflect"
"testing"
)
// 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 TestParser(t *testing.T) {
tests := []struct {
path string
steps []XPathStep
wantErr bool
}{
{
path: "/store/bicycle/color",
steps: []XPathStep{
{Type: RootStep},
{Type: ChildStep, Name: "store"},
{Type: ChildStep, Name: "bicycle"},
{Type: ChildStep, Name: "color"},
},
},
{
path: "//price",
steps: []XPathStep{
{Type: RootStep},
{Type: RecursiveDescentStep, Name: "price"},
},
},
{
path: "/store/book/*",
steps: []XPathStep{
{Type: RootStep},
{Type: ChildStep, Name: "store"},
{Type: ChildStep, Name: "book"},
{Type: WildcardStep},
},
},
{
path: "/store/book[1]/title",
steps: []XPathStep{
{Type: RootStep},
{Type: ChildStep, Name: "store"},
{Type: ChildStep, Name: "book"},
{Type: PredicateStep, Predicate: &Predicate{Type: IndexPredicate, Index: 1}},
{Type: ChildStep, Name: "title"},
},
},
{
path: "//title[@lang]",
steps: []XPathStep{
{Type: RootStep},
{Type: RecursiveDescentStep, Name: "title"},
{Type: PredicateStep, Predicate: &Predicate{Type: AttributeExistsPredicate, Attribute: "lang"}},
},
},
{
path: "//title[@lang='en']",
steps: []XPathStep{
{Type: RootStep},
{Type: RecursiveDescentStep, Name: "title"},
{Type: PredicateStep, Predicate: &Predicate{
Type: AttributeEqualsPredicate,
Attribute: "lang",
Value: "en",
}},
},
},
{
path: "/store/book[price>35.00]/title",
steps: []XPathStep{
{Type: RootStep},
{Type: ChildStep, Name: "store"},
{Type: ChildStep, Name: "book"},
{Type: PredicateStep, Predicate: &Predicate{
Type: ComparisonPredicate,
Expression: "price>35.00",
}},
{Type: ChildStep, Name: "title"},
},
},
{
path: "/store/book[last()]",
steps: []XPathStep{
{Type: RootStep},
{Type: ChildStep, Name: "store"},
{Type: ChildStep, Name: "book"},
{Type: PredicateStep, Predicate: &Predicate{Type: LastPredicate}},
},
},
{
path: "/store/book[last()-1]",
steps: []XPathStep{
{Type: RootStep},
{Type: ChildStep, Name: "store"},
{Type: ChildStep, Name: "book"},
{Type: PredicateStep, Predicate: &Predicate{
Type: LastMinusPredicate,
Offset: 1,
}},
},
},
{
path: "/store/book[position()<3]",
steps: []XPathStep{
{Type: RootStep},
{Type: ChildStep, Name: "store"},
{Type: ChildStep, Name: "book"},
{Type: PredicateStep, Predicate: &Predicate{
Type: PositionPredicate,
Expression: "position()<3",
}},
},
},
{
path: "invalid/path",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
steps, err := ParseXPath(tt.path)
if (err != nil) != tt.wantErr {
t.Fatalf("ParseXPath() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && !reflect.DeepEqual(steps, tt.steps) {
t.Errorf("ParseXPath() steps = %+v, want %+v", steps, tt.steps)
}
})
}
}
func TestEvaluator(t *testing.T) {
tests := []struct {
name string
path string
expected []XMLNode
error bool
}{
{
name: "simple_element_access",
path: "/store/bicycle/color",
expected: []XMLNode{
{Value: "red", Path: "/store/bicycle/color"},
},
},
{
name: "recursive_element_access",
path: "//price",
expected: []XMLNode{
{Value: "22.99", Path: "/store/book[1]/price"},
{Value: "23.45", Path: "/store/book[2]/price"},
{Value: "39.95", Path: "/store/book[3]/price"},
{Value: "199.95", Path: "/store/bicycle/price"},
},
},
{
name: "wildcard_element_access",
path: "/store/book[1]/*",
expected: []XMLNode{
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
{Value: "J.R.R. Tolkien", Path: "/store/book[1]/author"},
{Value: "1954", Path: "/store/book[1]/year"},
{Value: "22.99", Path: "/store/book[1]/price"},
},
},
{
name: "indexed_element_access",
path: "/store/book[1]/title",
expected: []XMLNode{
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
},
},
{
name: "attribute_exists_predicate",
path: "//title[@lang]",
expected: []XMLNode{
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
{Value: "The Two Towers", Path: "/store/book[2]/title"},
{Value: "Learning XML", Path: "/store/book[3]/title"},
},
},
{
name: "attribute_equals_predicate",
path: "//title[@lang='en']",
expected: []XMLNode{
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
{Value: "The Two Towers", Path: "/store/book[2]/title"},
{Value: "Learning XML", Path: "/store/book[3]/title"},
},
},
{
name: "value_comparison_predicate",
path: "/store/book[price>35.00]/title",
expected: []XMLNode{
{Value: "Learning XML", Path: "/store/book[3]/title"},
},
},
{
name: "last_predicate",
path: "/store/book[last()]/title",
expected: []XMLNode{
{Value: "Learning XML", Path: "/store/book[3]/title"},
},
},
{
name: "last_minus_predicate",
path: "/store/book[last()-1]/title",
expected: []XMLNode{
{Value: "The Two Towers", Path: "/store/book[2]/title"},
},
},
{
name: "position_predicate",
path: "/store/book[position()<3]/title",
expected: []XMLNode{
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
{Value: "The Two Towers", Path: "/store/book[2]/title"},
},
},
{
name: "all_elements",
path: "//*",
expected: []XMLNode{
// For brevity, we'll just check the count, not all values
},
},
{
name: "invalid_index",
path: "/store/book[10]/title",
expected: []XMLNode{},
error: true,
},
{
name: "nonexistent_element",
path: "/store/nonexistent",
expected: []XMLNode{},
error: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Get(testXML, tt.path)
if err != nil {
if !tt.error {
t.Errorf("Get() returned error: %v", err)
}
return
}
// Special handling for the "//*" test case
if tt.path == "//*" {
// Just check that we got multiple elements, not the specific count
if len(result) < 10 { // We expect at least 10 elements
t.Errorf("Expected multiple elements for '//*', got %d", len(result))
}
return
}
if len(result) != len(tt.expected) {
t.Errorf("Expected %d items, got %d", len(tt.expected), len(result))
return
}
// 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("", "/store/book")
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 := ParseXPath("")
if err == nil {
t.Error("Expected error for empty path")
}
})
t.Run("invalid_xml", func(t *testing.T) {
_, err := Get("<invalid>xml", "/store")
if err == nil {
t.Error("Expected error for invalid XML")
}
})
t.Run("current_node", func(t *testing.T) {
result, err := Get(testXML, "/store/book[1]/.")
if err != nil {
t.Errorf("Get() returned error: %v", err)
return
}
if len(result) != 1 {
t.Errorf("Expected 1 result, got %d", len(result))
}
})
t.Run("attributes", func(t *testing.T) {
result, err := Get(testXML, "/store/book[1]/title/@lang")
if err != nil {
t.Errorf("Get() returned error: %v", err)
return
}
if len(result) != 1 || result[0].Value != "en" {
t.Errorf("Expected 'en', got %v", result)
}
})
}
func TestGetWithPaths(t *testing.T) {
tests := []struct {
name string
path string
expected []XMLNode
}{
{
name: "simple_element_access",
path: "/store/bicycle/color",
expected: []XMLNode{
{Value: "red", Path: "/store/bicycle/color"},
},
},
{
name: "indexed_element_access",
path: "/store/book[1]/title",
expected: []XMLNode{
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
},
},
{
name: "recursive_element_access",
path: "//price",
expected: []XMLNode{
{Value: "22.99", Path: "/store/book[1]/price"},
{Value: "23.45", Path: "/store/book[2]/price"},
{Value: "39.95", Path: "/store/book[3]/price"},
{Value: "199.95", Path: "/store/bicycle/price"},
},
},
{
name: "attribute_access",
path: "/store/book[1]/title/@lang",
expected: []XMLNode{
{Value: "en", Path: "/store/book[1]/title/@lang"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Get(testXML, 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("Get() 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)
}
}
})
}
}
func TestSet(t *testing.T) {
t.Run("simple element", func(t *testing.T) {
xmlData := `<root><name>John</name></root>`
newXML, err := Set(xmlData, "/root/name", "Jane")
if err != nil {
t.Errorf("Set() returned error: %v", err)
return
}
// Verify the change
result, err := Get(newXML, "/root/name")
if err != nil {
t.Errorf("Get() returned error: %v", err)
return
}
if len(result) != 1 || result[0].Value != "Jane" {
t.Errorf("Set() failed: expected name to be 'Jane', got %v", result)
}
})
t.Run("attribute", func(t *testing.T) {
xmlData := `<root><element id="123"></element></root>`
newXML, err := Set(xmlData, "/root/element/@id", "456")
if err != nil {
t.Errorf("Set() returned error: %v", err)
return
}
// Verify the change
result, err := Get(newXML, "/root/element/@id")
if err != nil {
t.Errorf("Get() returned error: %v", err)
return
}
if len(result) != 1 || result[0].Value != "456" {
t.Errorf("Set() failed: expected id to be '456', got %v", result)
}
})
t.Run("indexed element", func(t *testing.T) {
xmlData := `<root><items><item>first</item><item>second</item></items></root>`
newXML, err := Set(xmlData, "/root/items/item[1]", "changed")
if err != nil {
t.Errorf("Set() returned error: %v", err)
return
}
// Verify the change
result, err := Get(newXML, "/root/items/item[1]")
if err != nil {
t.Errorf("Get() returned error: %v", err)
return
}
if len(result) != 1 || result[0].Value != "changed" {
t.Errorf("Set() failed: expected item to be 'changed', got %v", result)
}
})
}
func TestSetAll(t *testing.T) {
t.Run("multiple elements", func(t *testing.T) {
xmlData := `<root><items><item>first</item><item>second</item></items></root>`
newXML, err := SetAll(xmlData, "//item", "changed")
if err != nil {
t.Errorf("SetAll() returned error: %v", err)
return
}
// Verify all items are changed
result, err := Get(newXML, "//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
}
for i, node := range result {
if node.Value != "changed" {
t.Errorf("Item %d not changed, got %v", i+1, node.Value)
}
}
})
t.Run("attributes", func(t *testing.T) {
xmlData := `<root><item id="1"/><item id="2"/></root>`
newXML, err := SetAll(xmlData, "//item/@id", "new")
if err != nil {
t.Errorf("SetAll() returned error: %v", err)
return
}
// Verify all attributes are changed
result, err := Get(newXML, "//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 i, node := range result {
if node.Value != "new" {
t.Errorf("Attribute %d not changed, got %v", i+1, node.Value)
}
}
})
}