Begin to rework the json parsing
This commit is contained in:
@@ -5,10 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/PaesslerAG/jsonpath"
|
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +16,12 @@ type JSONProcessor struct{}
|
|||||||
// Process implements the Processor interface for JSONProcessor
|
// Process implements the Processor interface for JSONProcessor
|
||||||
func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
|
func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
|
||||||
// Read file content
|
// Read file content
|
||||||
fullPath := filepath.Join(".", filename)
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("error getting current working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(cwd, filename)
|
||||||
content, err := os.ReadFile(fullPath)
|
content, err := os.ReadFile(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
||||||
@@ -64,28 +67,15 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Lua
|
// Initialize Lua
|
||||||
L := lua.NewState()
|
L, err := NewLuaState()
|
||||||
|
if err != nil {
|
||||||
|
return content, 0, 0, fmt.Errorf("error creating Lua state: %v", err)
|
||||||
|
}
|
||||||
defer L.Close()
|
defer L.Close()
|
||||||
|
|
||||||
// Load math library
|
|
||||||
L.Push(L.GetGlobal("require"))
|
|
||||||
L.Push(lua.LString("math"))
|
|
||||||
if err := L.PCall(1, 1, nil); err != nil {
|
|
||||||
return content, 0, 0, fmt.Errorf("error loading Lua math library: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load helper functions
|
|
||||||
if err := InitLuaHelpers(L); err != nil {
|
|
||||||
return content, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply modifications to each node
|
// Apply modifications to each node
|
||||||
modCount := 0
|
modCount := 0
|
||||||
for i, value := range values {
|
for i, value := range values {
|
||||||
// Reset Lua state for each node
|
|
||||||
L.SetGlobal("v1", lua.LNil)
|
|
||||||
L.SetGlobal("s1", lua.LNil)
|
|
||||||
|
|
||||||
// Convert to Lua variables
|
// Convert to Lua variables
|
||||||
err = p.ToLua(L, value)
|
err = p.ToLua(L, value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -94,7 +84,7 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
|
|||||||
|
|
||||||
// Execute Lua script
|
// Execute Lua script
|
||||||
if err := L.DoString(luaExpr); err != nil {
|
if err := L.DoString(luaExpr); err != nil {
|
||||||
return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err)
|
return content, modCount, matchCount, fmt.Errorf("error executing Lua %s: %v", luaExpr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get modified value
|
// Get modified value
|
||||||
@@ -103,22 +93,26 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
|
|||||||
return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err)
|
return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if value didn't change
|
|
||||||
if fmt.Sprintf("%v", value) == fmt.Sprintf("%v", result) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the modification to the JSON data
|
// Apply the modification to the JSON data
|
||||||
err = p.updateJSONValue(jsonData, paths[i], result)
|
err = p.updateJSONValue(jsonData, paths[i], result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return content, modCount, matchCount, fmt.Errorf("error updating JSON: %v", err)
|
return content, modCount, matchCount, fmt.Errorf("error updating JSON: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Increment mod count if we haven't already counted object properties
|
||||||
modCount++
|
modCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the modified JSON back to a string
|
// Convert the modified JSON back to a string with same formatting
|
||||||
jsonBytes, err := json.MarshalIndent(jsonData, "", " ")
|
var jsonBytes []byte
|
||||||
|
if indent, err := detectJsonIndentation(content); err == nil && indent != "" {
|
||||||
|
// Use detected indentation for output formatting
|
||||||
|
jsonBytes, err = json.MarshalIndent(jsonData, "", indent)
|
||||||
|
} else {
|
||||||
|
// Fall back to standard 2-space indent
|
||||||
|
jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return content, modCount, matchCount, fmt.Errorf("error serializing JSON: %v", err)
|
return content, modCount, matchCount, fmt.Errorf("error serializing JSON: %v", err)
|
||||||
}
|
}
|
||||||
@@ -126,183 +120,88 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
|
|||||||
return string(jsonBytes), modCount, matchCount, nil
|
return string(jsonBytes), modCount, matchCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// findJSONPaths finds all JSON paths and their values that match the given JSONPath expression
|
// detectJsonIndentation tries to determine the indentation used in the original JSON
|
||||||
func (p *JSONProcessor) findJSONPaths(jsonData interface{}, pattern string) ([]string, []interface{}, error) {
|
func detectJsonIndentation(content string) (string, error) {
|
||||||
// Extract all matching values using JSONPath
|
lines := strings.Split(content, "\n")
|
||||||
values, err := jsonpath.Get(pattern, jsonData)
|
if len(lines) < 2 {
|
||||||
if err != nil {
|
return "", fmt.Errorf("not enough lines to detect indentation")
|
||||||
return nil, nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert values to a slice if it's not already
|
// Look for the first indented line
|
||||||
valuesSlice := []interface{}{}
|
for i := 1; i < len(lines); i++ {
|
||||||
paths := []string{}
|
line := lines[i]
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
switch v := values.(type) {
|
if trimmed == "" {
|
||||||
case []interface{}:
|
continue
|
||||||
valuesSlice = v
|
}
|
||||||
// Generate paths for array elements
|
|
||||||
// This is simplified - for complex JSONPath expressions you might
|
// Calculate leading whitespace
|
||||||
// need a more robust approach to generate the exact path
|
indent := line[:len(line)-len(trimmed)]
|
||||||
basePath := pattern
|
if len(indent) > 0 {
|
||||||
if strings.Contains(pattern, "[*]") || strings.HasSuffix(pattern, ".*") {
|
return indent, nil
|
||||||
basePath = strings.Replace(pattern, "[*]", "", -1)
|
|
||||||
basePath = strings.Replace(basePath, ".*", "", -1)
|
|
||||||
for i := 0; i < len(v); i++ {
|
|
||||||
paths = append(paths, fmt.Sprintf("%s[%d]", basePath, i))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for range v {
|
|
||||||
paths = append(paths, pattern)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
valuesSlice = append(valuesSlice, v)
|
|
||||||
paths = append(paths, pattern)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return paths, valuesSlice, nil
|
return "", fmt.Errorf("no indentation detected")
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateJSONValue updates a value in the JSON data structure at the given path
|
func (p *JSONProcessor) findJSONPaths(jsonData interface{}, pattern string) ([]string, []interface{}, error) {
|
||||||
|
// / $ the root object/element
|
||||||
|
// // .. recursive descent. JSONPath borrows this syntax from E4X.
|
||||||
|
// * * wildcard. All objects/elements regardless their names.
|
||||||
|
// [] [] subscript operator. XPath uses it to iterate over element collections and for predicates. In Javascript and JSON it is the native array operator.
|
||||||
|
|
||||||
|
// patternPaths := strings.Split(pattern, ".")
|
||||||
|
// current := jsonData
|
||||||
|
// for _, path := range patternPaths {
|
||||||
|
// switch path {
|
||||||
|
// case "$":
|
||||||
|
// current = jsonData
|
||||||
|
// case "@":
|
||||||
|
// current = jsonData
|
||||||
|
// case "*":
|
||||||
|
// current = jsonData
|
||||||
|
// case "..":
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// paths, values, err := p.findJSONPaths(jsonData, pattern)
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// / Selects from the root node
|
||||||
|
// // Selects nodes in the document from the current node that match the selection no matter where they are
|
||||||
|
// . Selects the current node
|
||||||
|
// @ Selects attributes
|
||||||
|
|
||||||
|
// /bookstore/* Selects all the child element nodes of the bookstore element
|
||||||
|
// //* Selects all elements in the document
|
||||||
|
|
||||||
|
// /bookstore/book[1] Selects the first book element that is the child of the bookstore element.
|
||||||
|
// /bookstore/book[last()] Selects the last book element that is the child of the bookstore element
|
||||||
|
// /bookstore/book[last()-1] Selects the last but one book element that is the child of the bookstore element
|
||||||
|
// /bookstore/book[position()<3] Selects the first two book elements that are children of the bookstore element
|
||||||
|
// //title[@lang] Selects all the title elements that have an attribute named lang
|
||||||
|
// //title[@lang='en'] Selects all the title elements that have a "lang" attribute with a value of "en"
|
||||||
|
// /bookstore/book[price>35.00] Selects all the book elements of the bookstore element that have a price element with a value greater than 35.00
|
||||||
|
// /bookstore/book[price>35.00]/title Selects all the title elements of the book elements of the bookstore element that have a price element with a value greater than 35.00
|
||||||
|
|
||||||
|
// 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 {
|
||||||
// This is a simplified approach - for a production system you'd need a more robust solution
|
|
||||||
// that can handle all JSONPath expressions
|
|
||||||
parts := strings.Split(path, ".")
|
|
||||||
current := jsonData
|
|
||||||
|
|
||||||
// Traverse the JSON structure
|
|
||||||
for i, part := range parts {
|
|
||||||
if i == len(parts)-1 {
|
|
||||||
// Last part, set the value
|
|
||||||
if strings.HasSuffix(part, "]") {
|
|
||||||
// Handle array access
|
|
||||||
arrayPart := part[:strings.Index(part, "[")]
|
|
||||||
indexPart := part[strings.Index(part, "[")+1 : strings.Index(part, "]")]
|
|
||||||
index, err := strconv.Atoi(indexPart)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid array index: %s", indexPart)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the array
|
|
||||||
var array []interface{}
|
|
||||||
if arrayPart == "" {
|
|
||||||
// Direct array access
|
|
||||||
array, _ = current.([]interface{})
|
|
||||||
} else {
|
|
||||||
// Access array property
|
|
||||||
obj, _ := current.(map[string]interface{})
|
|
||||||
array, _ = obj[arrayPart].([]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the value
|
|
||||||
if index >= 0 && index < len(array) {
|
|
||||||
array[index] = newValue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Handle object property
|
|
||||||
obj, _ := current.(map[string]interface{})
|
|
||||||
obj[part] = newValue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not the last part, continue traversing
|
|
||||||
if strings.HasSuffix(part, "]") {
|
|
||||||
// Handle array access
|
|
||||||
arrayPart := part[:strings.Index(part, "[")]
|
|
||||||
indexPart := part[strings.Index(part, "[")+1 : strings.Index(part, "]")]
|
|
||||||
index, err := strconv.Atoi(indexPart)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid array index: %s", indexPart)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the array
|
|
||||||
var array []interface{}
|
|
||||||
if arrayPart == "" {
|
|
||||||
// Direct array access
|
|
||||||
array, _ = current.([]interface{})
|
|
||||||
} else {
|
|
||||||
// Access array property
|
|
||||||
obj, _ := current.(map[string]interface{})
|
|
||||||
array, _ = obj[arrayPart].([]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue with the array element
|
|
||||||
if index >= 0 && index < len(array) {
|
|
||||||
current = array[index]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Handle object property
|
|
||||||
obj, _ := current.(map[string]interface{})
|
|
||||||
current = obj[part]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToLua converts JSON values to Lua variables
|
// ToLua converts JSON values to Lua variables
|
||||||
func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
|
func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
|
||||||
switch v := data.(type) {
|
table, err := ToLuaTable(L, data)
|
||||||
case float64:
|
if err != nil {
|
||||||
L.SetGlobal("v1", lua.LNumber(v))
|
return err
|
||||||
L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", v)))
|
|
||||||
case int:
|
|
||||||
L.SetGlobal("v1", lua.LNumber(v))
|
|
||||||
L.SetGlobal("s1", lua.LString(fmt.Sprintf("%d", v)))
|
|
||||||
case string:
|
|
||||||
L.SetGlobal("s1", lua.LString(v))
|
|
||||||
// Try to convert to number if possible
|
|
||||||
if val, err := strconv.ParseFloat(v, 64); err == nil {
|
|
||||||
L.SetGlobal("v1", lua.LNumber(val))
|
|
||||||
} else {
|
|
||||||
L.SetGlobal("v1", lua.LNumber(0))
|
|
||||||
}
|
|
||||||
case bool:
|
|
||||||
if v {
|
|
||||||
L.SetGlobal("v1", lua.LNumber(1))
|
|
||||||
} else {
|
|
||||||
L.SetGlobal("v1", lua.LNumber(0))
|
|
||||||
}
|
|
||||||
L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", v)))
|
|
||||||
default:
|
|
||||||
// For complex types, convert to string
|
|
||||||
L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", v)))
|
|
||||||
L.SetGlobal("v1", lua.LNumber(0))
|
|
||||||
}
|
}
|
||||||
|
L.SetGlobal("v", table)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromLua retrieves values from Lua
|
// FromLua retrieves values from Lua
|
||||||
func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
||||||
// Check if string variable was modified
|
luaValue := L.GetGlobal("v")
|
||||||
s1 := L.GetGlobal("s1")
|
return FromLuaTable(L, luaValue.(*lua.LTable))
|
||||||
if s1 != lua.LNil {
|
|
||||||
if s1Str, ok := s1.(lua.LString); ok {
|
|
||||||
// Try to convert to number if it's numeric
|
|
||||||
if val, err := strconv.ParseFloat(string(s1Str), 64); err == nil {
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
// If it's "true" or "false", convert to boolean
|
|
||||||
if string(s1Str) == "true" {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
if string(s1Str) == "false" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return string(s1Str), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if numeric variable was modified
|
|
||||||
v1 := L.GetGlobal("v1")
|
|
||||||
if v1 != lua.LNil {
|
|
||||||
if v1Num, ok := v1.(lua.LNumber); ok {
|
|
||||||
return float64(v1Num), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default return nil
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/PaesslerAG/jsonpath"
|
"github.com/PaesslerAG/jsonpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// findMatchingPaths finds nodes in a JSON document that match the given JSONPath
|
// fi ndMatchingPaths finds nodes in a JSON document that match the given JSONPath
|
||||||
func findMatchingPaths(jsonDoc interface{}, path string) ([]interface{}, error) {
|
func findMatchingPaths(jsonDoc interface{}, path string) ([]interface{}, error) {
|
||||||
// Use the existing jsonpath library to extract values
|
// Use the existing jsonpath library to extract values
|
||||||
result, err := jsonpath.Get(path, jsonDoc)
|
result, err := jsonpath.Get(path, jsonDoc)
|
||||||
@@ -57,7 +57,7 @@ func TestJSONProcessor_Process_NumericValues(t *testing.T) {
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
p := &JSONProcessor{}
|
p := &JSONProcessor{}
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "$.books[*].price", "v=v*2")
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.books[*]", "v.price=v.price*2")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing content: %v", err)
|
t.Fatalf("Error processing content: %v", err)
|
||||||
@@ -71,12 +71,45 @@ func TestJSONProcessor_Process_NumericValues(t *testing.T) {
|
|||||||
t.Errorf("Expected 2 modifications, got %d", modCount)
|
t.Errorf("Expected 2 modifications, got %d", modCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize whitespace for comparison
|
// Compare parsed JSON objects instead of formatted strings
|
||||||
normalizedResult := normalizeWhitespace(result)
|
var resultObj map[string]interface{}
|
||||||
normalizedExpected := normalizeWhitespace(expected)
|
if err := json.Unmarshal([]byte(result), &resultObj); err != nil {
|
||||||
|
t.Fatalf("Failed to parse result JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if normalizedResult != normalizedExpected {
|
var expectedObj map[string]interface{}
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
if err := json.Unmarshal([]byte(expected), &expectedObj); err != nil {
|
||||||
|
t.Fatalf("Failed to parse expected JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the first book's price
|
||||||
|
resultBooks, ok := resultObj["books"].([]interface{})
|
||||||
|
if !ok || len(resultBooks) < 1 {
|
||||||
|
t.Fatalf("Expected books array in result")
|
||||||
|
}
|
||||||
|
resultBook1, ok := resultBooks[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected first book to be an object")
|
||||||
|
}
|
||||||
|
resultPrice1, ok := resultBook1["price"].(float64)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected numeric price in first book")
|
||||||
|
}
|
||||||
|
if resultPrice1 != 89.9 {
|
||||||
|
t.Errorf("Expected first book price to be 89.9, got %v", resultPrice1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the second book's price
|
||||||
|
resultBook2, ok := resultBooks[1].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected second book to be an object")
|
||||||
|
}
|
||||||
|
resultPrice2, ok := resultBook2["price"].(float64)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected numeric price in second book")
|
||||||
|
}
|
||||||
|
if resultPrice2 != 11.9 {
|
||||||
|
t.Errorf("Expected second book price to be 11.9, got %v", resultPrice2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +124,15 @@ func TestJSONProcessor_Process_StringValues(t *testing.T) {
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
p := &JSONProcessor{}
|
p := &JSONProcessor{}
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "$.config.*", "v=v*2")
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.config", "for k,vi in pairs(v) do v[k]=vi*2 end")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing content: %v", err)
|
t.Fatalf("Error processing content: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug info
|
||||||
|
t.Logf("Result: %s", result)
|
||||||
|
t.Logf("Match count: %d, Mod count: %d", matchCount, modCount)
|
||||||
|
|
||||||
if matchCount != 3 {
|
if matchCount != 3 {
|
||||||
t.Errorf("Expected 3 matches, got %d", matchCount)
|
t.Errorf("Expected 3 matches, got %d", matchCount)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user