Refactor everything to processors and implement json and xml processors such as they are
This commit is contained in:
609
processor/json.go
Normal file
609
processor/json.go
Normal file
@@ -0,0 +1,609 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// JSONProcessor implements the Processor interface using JSONPath
|
||||
type JSONProcessor struct {
|
||||
Logger Logger
|
||||
}
|
||||
|
||||
// NewJSONProcessor creates a new JSONProcessor
|
||||
func NewJSONProcessor(logger Logger) *JSONProcessor {
|
||||
return &JSONProcessor{
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Process implements the Processor interface for JSONProcessor
|
||||
func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string, originalExpr string) (int, int, error) {
|
||||
// Use pattern as JSONPath expression
|
||||
jsonPathExpr := pattern
|
||||
|
||||
// Read file content
|
||||
fullPath := filepath.Join(".", filename)
|
||||
content, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
||||
}
|
||||
|
||||
fileContent := string(content)
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("File %s loaded: %d bytes", fullPath, len(content))
|
||||
}
|
||||
|
||||
// Process the content
|
||||
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, jsonPathExpr, luaExpr, originalExpr)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// If we made modifications, save the file
|
||||
if modCount > 0 {
|
||||
err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("error writing file: %v", err)
|
||||
}
|
||||
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("Made %d JSON value modifications to %s and saved (%d bytes)",
|
||||
modCount, fullPath, len(modifiedContent))
|
||||
}
|
||||
} else if p.Logger != nil {
|
||||
p.Logger.Printf("No modifications made to %s", fullPath)
|
||||
}
|
||||
|
||||
return modCount, matchCount, nil
|
||||
}
|
||||
|
||||
// ToLua implements the Processor interface for JSONProcessor
|
||||
func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
|
||||
// For JSON, convert different types to appropriate Lua types
|
||||
return nil
|
||||
}
|
||||
|
||||
// FromLua implements the Processor interface for JSONProcessor
|
||||
func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
||||
// Extract changes from Lua environment
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ProcessContent implements the Processor interface for JSONProcessor
|
||||
// It processes JSON content directly without file I/O
|
||||
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string, originalExpr string) (string, int, int, error) {
|
||||
// Parse JSON
|
||||
var jsonDoc interface{}
|
||||
err := json.Unmarshal([]byte(content), &jsonDoc)
|
||||
if err != nil {
|
||||
return "", 0, 0, fmt.Errorf("error parsing JSON: %v", err)
|
||||
}
|
||||
|
||||
// Log the JSONPath expression we're using
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("JSON mode selected with JSONPath expression: %s", pattern)
|
||||
}
|
||||
|
||||
// Initialize Lua state
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
// Setup Lua helper functions
|
||||
if err := InitLuaHelpers(L); err != nil {
|
||||
return "", 0, 0, err
|
||||
}
|
||||
|
||||
// Setup JSON helpers
|
||||
p.SetupJSONHelpers(L)
|
||||
|
||||
// Find matching nodes with simple JSONPath implementation
|
||||
matchingPaths, err := p.findNodePaths(jsonDoc, pattern)
|
||||
if err != nil {
|
||||
return "", 0, 0, fmt.Errorf("error finding JSON nodes: %v", err)
|
||||
}
|
||||
|
||||
if len(matchingPaths) == 0 {
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("No JSON nodes matched JSONPath expression: %s", pattern)
|
||||
}
|
||||
return content, 0, 0, nil
|
||||
}
|
||||
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("Found %d JSON nodes matching the path", len(matchingPaths))
|
||||
}
|
||||
|
||||
// Process each node
|
||||
matchCount := len(matchingPaths)
|
||||
modificationCount := 0
|
||||
modifications := []ModificationRecord{}
|
||||
|
||||
// Clone the document for modification
|
||||
var modifiedDoc interface{}
|
||||
modifiedBytes, err := json.Marshal(jsonDoc)
|
||||
if err != nil {
|
||||
return "", 0, 0, fmt.Errorf("error cloning JSON document: %v", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(modifiedBytes, &modifiedDoc)
|
||||
if err != nil {
|
||||
return "", 0, 0, fmt.Errorf("error cloning JSON document: %v", err)
|
||||
}
|
||||
|
||||
// For each matching path, extract value, apply Lua script, and update
|
||||
for i, path := range matchingPaths {
|
||||
// Extract the original value
|
||||
originalValue, err := p.getValueAtPath(jsonDoc, path)
|
||||
if err != nil || originalValue == nil {
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("Error getting value at path %v: %v", path, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("Processing node #%d at path %v with value: %v", i+1, path, originalValue)
|
||||
}
|
||||
|
||||
// Process based on the value type
|
||||
switch val := originalValue.(type) {
|
||||
case float64:
|
||||
// Set up Lua environment for numeric value
|
||||
L.SetGlobal("v1", lua.LNumber(val))
|
||||
L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", val)))
|
||||
|
||||
// Execute Lua script
|
||||
if err := L.DoString(luaExpr); err != nil {
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract modified value
|
||||
modVal := L.GetGlobal("v1")
|
||||
if v, ok := modVal.(lua.LNumber); ok {
|
||||
newValue := float64(v)
|
||||
|
||||
// Update the value in the document only if it changed
|
||||
if newValue != val {
|
||||
err := p.setValueAtPath(modifiedDoc, path, newValue)
|
||||
if err != nil {
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("Error updating value at path %v: %v", path, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
modificationCount++
|
||||
modifications = append(modifications, ModificationRecord{
|
||||
File: "",
|
||||
OldValue: fmt.Sprintf("%v", val),
|
||||
NewValue: fmt.Sprintf("%v", newValue),
|
||||
Operation: originalExpr,
|
||||
Context: fmt.Sprintf("(JSONPath: %s)", pattern),
|
||||
})
|
||||
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("Modified numeric node #%d: %v -> %v", i+1, val, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case string:
|
||||
// Set up Lua environment for string value
|
||||
L.SetGlobal("s1", lua.LString(val))
|
||||
|
||||
// Try to convert to number if possible
|
||||
if floatVal, err := strconv.ParseFloat(val, 64); err == nil {
|
||||
L.SetGlobal("v1", lua.LNumber(floatVal))
|
||||
} else {
|
||||
L.SetGlobal("v1", lua.LNumber(0)) // Default to 0 if not numeric
|
||||
}
|
||||
|
||||
// Execute Lua script
|
||||
if err := L.DoString(luaExpr); err != nil {
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for modifications in string (s1) or numeric (v1) values
|
||||
var newValue interface{}
|
||||
modified := false
|
||||
|
||||
// Check if s1 was modified
|
||||
sVal := L.GetGlobal("s1")
|
||||
if s, ok := sVal.(lua.LString); ok && string(s) != val {
|
||||
newValue = string(s)
|
||||
modified = true
|
||||
} else {
|
||||
// Check if v1 was modified to a number
|
||||
vVal := L.GetGlobal("v1")
|
||||
if v, ok := vVal.(lua.LNumber); ok {
|
||||
numStr := strconv.FormatFloat(float64(v), 'f', -1, 64)
|
||||
if numStr != val {
|
||||
newValue = numStr
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the modification if anything changed
|
||||
if modified {
|
||||
err := p.setValueAtPath(modifiedDoc, path, newValue)
|
||||
if err != nil {
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("Error updating value at path %v: %v", path, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
modificationCount++
|
||||
modifications = append(modifications, ModificationRecord{
|
||||
File: "",
|
||||
OldValue: val,
|
||||
NewValue: fmt.Sprintf("%v", newValue),
|
||||
Operation: originalExpr,
|
||||
Context: fmt.Sprintf("(JSONPath: %s)", pattern),
|
||||
})
|
||||
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("Modified string node #%d: '%s' -> '%s'",
|
||||
i+1, LimitString(val, 30), LimitString(fmt.Sprintf("%v", newValue), 30))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal the modified document back to JSON with indentation
|
||||
if modificationCount > 0 {
|
||||
modifiedJSON, err := json.MarshalIndent(modifiedDoc, "", " ")
|
||||
if err != nil {
|
||||
return "", 0, 0, fmt.Errorf("error marshaling modified JSON: %v", err)
|
||||
}
|
||||
|
||||
if p.Logger != nil {
|
||||
p.Logger.Printf("Made %d JSON node modifications", modificationCount)
|
||||
}
|
||||
|
||||
return string(modifiedJSON), modificationCount, matchCount, nil
|
||||
}
|
||||
|
||||
// If no modifications were made, return the original content
|
||||
return content, 0, matchCount, nil
|
||||
}
|
||||
|
||||
// findNodePaths implements a simplified JSONPath for finding paths to nodes
|
||||
func (p *JSONProcessor) findNodePaths(doc interface{}, path string) ([][]interface{}, error) {
|
||||
// Validate the path has proper syntax
|
||||
if strings.Contains(path, "[[") || strings.Contains(path, "]]") {
|
||||
return nil, fmt.Errorf("invalid JSONPath syntax: %s", path)
|
||||
}
|
||||
|
||||
// Handle root element special case
|
||||
if path == "$" {
|
||||
return [][]interface{}{{doc}}, nil
|
||||
}
|
||||
|
||||
// Split path into segments
|
||||
segments := strings.Split(strings.TrimPrefix(path, "$."), ".")
|
||||
|
||||
// Start with the root
|
||||
current := [][]interface{}{{doc}}
|
||||
|
||||
// Process each segment
|
||||
for _, segment := range segments {
|
||||
var next [][]interface{}
|
||||
|
||||
// Handle array notation [*]
|
||||
if segment == "[*]" || strings.HasSuffix(segment, "[*]") {
|
||||
baseName := strings.TrimSuffix(segment, "[*]")
|
||||
|
||||
for _, path := range current {
|
||||
item := path[len(path)-1] // Get the last item in the path
|
||||
|
||||
switch v := item.(type) {
|
||||
case map[string]interface{}:
|
||||
if baseName == "" {
|
||||
// [*] means all elements at this level
|
||||
for _, val := range v {
|
||||
if arr, ok := val.([]interface{}); ok {
|
||||
for i, elem := range arr {
|
||||
newPath := make([]interface{}, len(path)+2)
|
||||
copy(newPath, path)
|
||||
newPath[len(path)] = i // Array index
|
||||
newPath[len(path)+1] = elem
|
||||
next = append(next, newPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if arr, ok := v[baseName].([]interface{}); ok {
|
||||
for i, elem := range arr {
|
||||
newPath := make([]interface{}, len(path)+3)
|
||||
copy(newPath, path)
|
||||
newPath[len(path)] = baseName
|
||||
newPath[len(path)+1] = i // Array index
|
||||
newPath[len(path)+2] = elem
|
||||
next = append(next, newPath)
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for i, elem := range v {
|
||||
newPath := make([]interface{}, len(path)+1)
|
||||
copy(newPath, path)
|
||||
newPath[len(path)-1] = i // Replace last elem with index
|
||||
newPath[len(path)] = elem
|
||||
next = append(next, newPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current = next
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle specific array indices
|
||||
if strings.Contains(segment, "[") && strings.Contains(segment, "]") {
|
||||
// Validate proper array syntax
|
||||
if !regexp.MustCompile(`\[\d+\]$`).MatchString(segment) {
|
||||
return nil, fmt.Errorf("invalid array index in JSONPath: %s", segment)
|
||||
}
|
||||
|
||||
// Extract base name and index
|
||||
baseName := segment[:strings.Index(segment, "[")]
|
||||
idxStr := segment[strings.Index(segment, "[")+1 : strings.Index(segment, "]")]
|
||||
idx, err := strconv.Atoi(idxStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid array index: %s", idxStr)
|
||||
}
|
||||
|
||||
for _, path := range current {
|
||||
item := path[len(path)-1] // Get the last item in the path
|
||||
|
||||
if obj, ok := item.(map[string]interface{}); ok {
|
||||
if arr, ok := obj[baseName].([]interface{}); ok && idx < len(arr) {
|
||||
newPath := make([]interface{}, len(path)+3)
|
||||
copy(newPath, path)
|
||||
newPath[len(path)] = baseName
|
||||
newPath[len(path)+1] = idx
|
||||
newPath[len(path)+2] = arr[idx]
|
||||
next = append(next, newPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current = next
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle regular object properties
|
||||
for _, path := range current {
|
||||
item := path[len(path)-1] // Get the last item in the path
|
||||
|
||||
if obj, ok := item.(map[string]interface{}); ok {
|
||||
if val, exists := obj[segment]; exists {
|
||||
newPath := make([]interface{}, len(path)+2)
|
||||
copy(newPath, path)
|
||||
newPath[len(path)] = segment
|
||||
newPath[len(path)+1] = val
|
||||
next = append(next, newPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current = next
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
|
||||
// getValueAtPath extracts a value from a JSON document at the specified path
|
||||
func (p *JSONProcessor) getValueAtPath(doc interface{}, path []interface{}) (interface{}, error) {
|
||||
if len(path) == 0 {
|
||||
return nil, fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
// The last element in the path is the value itself
|
||||
return path[len(path)-1], nil
|
||||
}
|
||||
|
||||
// setValueAtPath updates a value in a JSON document at the specified path
|
||||
func (p *JSONProcessor) setValueAtPath(doc interface{}, path []interface{}, newValue interface{}) error {
|
||||
if len(path) < 2 {
|
||||
return fmt.Errorf("path too short to update value")
|
||||
}
|
||||
|
||||
// The path structure alternates: object/key/object/key/.../finalObject/finalKey/value
|
||||
// We need to navigate to the object containing our key
|
||||
// We'll get the parent object and the key to modify
|
||||
|
||||
// Find the parent object (second to last object) and the key (last object's property name)
|
||||
// For the path structure, the parent is at index len-3 and key at len-2
|
||||
if len(path) < 3 {
|
||||
// Simple case: directly update the root object
|
||||
rootObj, ok := doc.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("root is not an object, cannot update")
|
||||
}
|
||||
|
||||
// Key should be a string
|
||||
key, ok := path[len(path)-2].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("key is not a string: %v", path[len(path)-2])
|
||||
}
|
||||
|
||||
rootObj[key] = newValue
|
||||
return nil
|
||||
}
|
||||
|
||||
// More complex case: we need to navigate to the parent object
|
||||
parentIdx := len(path) - 3
|
||||
keyIdx := len(path) - 2
|
||||
|
||||
// The actual key we need to modify
|
||||
key, isString := path[keyIdx].(string)
|
||||
keyInt, isInt := path[keyIdx].(int)
|
||||
|
||||
if !isString && !isInt {
|
||||
return fmt.Errorf("key must be string or int, got %T", path[keyIdx])
|
||||
}
|
||||
|
||||
// Get the parent object that contains the key
|
||||
parent := path[parentIdx]
|
||||
|
||||
// If parent is a map, use string key
|
||||
if parentMap, ok := parent.(map[string]interface{}); ok && isString {
|
||||
parentMap[key] = newValue
|
||||
return nil
|
||||
}
|
||||
|
||||
// If parent is an array, use int key
|
||||
if parentArray, ok := parent.([]interface{}); ok && isInt {
|
||||
if keyInt < 0 || keyInt >= len(parentArray) {
|
||||
return fmt.Errorf("array index %d out of bounds [0,%d)", keyInt, len(parentArray))
|
||||
}
|
||||
parentArray[keyInt] = newValue
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot update value: parent is %T and key is %T", parent, path[keyIdx])
|
||||
}
|
||||
|
||||
// SetupJSONHelpers adds JSON-specific helper functions to Lua
|
||||
func (p *JSONProcessor) SetupJSONHelpers(L *lua.LState) {
|
||||
// Helper to get type of JSON value
|
||||
L.SetGlobal("json_type", L.NewFunction(func(L *lua.LState) int {
|
||||
// Get the value passed to the function
|
||||
val := L.Get(1)
|
||||
|
||||
// Determine type
|
||||
switch val.Type() {
|
||||
case lua.LTNil:
|
||||
L.Push(lua.LString("null"))
|
||||
case lua.LTBool:
|
||||
L.Push(lua.LString("boolean"))
|
||||
case lua.LTNumber:
|
||||
L.Push(lua.LString("number"))
|
||||
case lua.LTString:
|
||||
L.Push(lua.LString("string"))
|
||||
case lua.LTTable:
|
||||
// Could be object or array - check for numeric keys
|
||||
isArray := true
|
||||
table := val.(*lua.LTable)
|
||||
table.ForEach(func(key, value lua.LValue) {
|
||||
if key.Type() != lua.LTNumber {
|
||||
isArray = false
|
||||
}
|
||||
})
|
||||
|
||||
if isArray {
|
||||
L.Push(lua.LString("array"))
|
||||
} else {
|
||||
L.Push(lua.LString("object"))
|
||||
}
|
||||
default:
|
||||
L.Push(lua.LString("unknown"))
|
||||
}
|
||||
|
||||
return 1
|
||||
}))
|
||||
}
|
||||
|
||||
// jsonToLua converts a Go JSON value to a Lua value
|
||||
func (p *JSONProcessor) jsonToLua(L *lua.LState, val interface{}) lua.LValue {
|
||||
if val == nil {
|
||||
return lua.LNil
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case bool:
|
||||
return lua.LBool(v)
|
||||
case float64:
|
||||
return lua.LNumber(v)
|
||||
case string:
|
||||
return lua.LString(v)
|
||||
case []interface{}:
|
||||
arr := L.NewTable()
|
||||
for i, item := range v {
|
||||
arr.RawSetInt(i+1, p.jsonToLua(L, item))
|
||||
}
|
||||
return arr
|
||||
case map[string]interface{}:
|
||||
obj := L.NewTable()
|
||||
for k, item := range v {
|
||||
obj.RawSetString(k, p.jsonToLua(L, item))
|
||||
}
|
||||
return obj
|
||||
default:
|
||||
// For unknown types, convert to string representation
|
||||
return lua.LString(fmt.Sprintf("%v", val))
|
||||
}
|
||||
}
|
||||
|
||||
// luaToJSON converts a Lua value to a Go JSON-compatible value
|
||||
func (p *JSONProcessor) luaToJSON(val lua.LValue) interface{} {
|
||||
switch val.Type() {
|
||||
case lua.LTNil:
|
||||
return nil
|
||||
case lua.LTBool:
|
||||
return lua.LVAsBool(val)
|
||||
case lua.LTNumber:
|
||||
return float64(val.(lua.LNumber))
|
||||
case lua.LTString:
|
||||
return val.String()
|
||||
case lua.LTTable:
|
||||
table := val.(*lua.LTable)
|
||||
|
||||
// Check if it's an array or an object
|
||||
isArray := true
|
||||
maxN := 0
|
||||
|
||||
table.ForEach(func(key, _ lua.LValue) {
|
||||
if key.Type() == lua.LTNumber {
|
||||
n := int(key.(lua.LNumber))
|
||||
if n > maxN {
|
||||
maxN = n
|
||||
}
|
||||
} else {
|
||||
isArray = false
|
||||
}
|
||||
})
|
||||
|
||||
if isArray && maxN > 0 {
|
||||
// It's an array
|
||||
arr := make([]interface{}, maxN)
|
||||
for i := 1; i <= maxN; i++ {
|
||||
item := table.RawGetInt(i)
|
||||
if item != lua.LNil {
|
||||
arr[i-1] = p.luaToJSON(item)
|
||||
}
|
||||
}
|
||||
return arr
|
||||
} else {
|
||||
// It's an object
|
||||
obj := make(map[string]interface{})
|
||||
table.ForEach(func(key, value lua.LValue) {
|
||||
if key.Type() == lua.LTString {
|
||||
obj[key.String()] = p.luaToJSON(value)
|
||||
} else {
|
||||
// Convert key to string if it's not already
|
||||
obj[fmt.Sprintf("%v", key)] = p.luaToJSON(value)
|
||||
}
|
||||
})
|
||||
return obj
|
||||
}
|
||||
default:
|
||||
// For functions, userdata, etc., convert to string
|
||||
return val.String()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user