566 lines
17 KiB
Go
566 lines
17 KiB
Go
package processor
|
|
|
|
import (
|
|
"cook/utils"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
|
"github.com/tidwall/gjson"
|
|
lua "github.com/yuin/gopher-lua"
|
|
)
|
|
|
|
// jsonLogger is a scoped logger for the processor/json package.
|
|
var jsonLogger = logger.Default.WithPrefix("processor/json")
|
|
|
|
// ProcessJSON applies Lua processing to JSON content
|
|
func ProcessJSON(content string, command utils.ModifyCommand, filename string) ([]utils.ReplaceCommand, error) {
|
|
processJsonLogger := jsonLogger.WithPrefix("ProcessJSON").WithField("commandName", command.Name).WithField("file", filename)
|
|
processJsonLogger.Debug("Starting JSON processing for file")
|
|
processJsonLogger.Trace("Initial file content length: %d", len(content))
|
|
|
|
var commands []utils.ReplaceCommand
|
|
startTime := time.Now()
|
|
|
|
// Parse JSON content
|
|
var jsonData interface{}
|
|
err := json.Unmarshal([]byte(content), &jsonData)
|
|
if err != nil {
|
|
processJsonLogger.Error("Failed to parse JSON content: %v", err)
|
|
return commands, fmt.Errorf("failed to parse JSON: %v", err)
|
|
}
|
|
processJsonLogger.Debug("Successfully parsed JSON content")
|
|
|
|
// Create Lua state
|
|
L, err := NewLuaState()
|
|
if err != nil {
|
|
processJsonLogger.Error("Error creating Lua state: %v", err)
|
|
return commands, fmt.Errorf("error creating Lua state: %v", err)
|
|
}
|
|
defer L.Close()
|
|
|
|
// Set filename global
|
|
L.SetGlobal("file", lua.LString(filename))
|
|
|
|
// Convert JSON data to Lua table
|
|
luaTable, err := ToLuaTable(L, jsonData)
|
|
if err != nil {
|
|
processJsonLogger.Error("Failed to convert JSON to Lua table: %v", err)
|
|
return commands, fmt.Errorf("failed to convert JSON to Lua table: %v", err)
|
|
}
|
|
|
|
// Set the JSON data as a global variable
|
|
L.SetGlobal("data", luaTable)
|
|
processJsonLogger.Debug("Set JSON data as Lua global 'data'")
|
|
|
|
// Build and execute Lua script for JSON mode
|
|
luaExpr := BuildJSONLuaScript(command.Lua)
|
|
processJsonLogger.Debug("Built Lua script from expression: %q", command.Lua)
|
|
processJsonLogger.Trace("Full Lua script: %q", utils.LimitString(luaExpr, 200))
|
|
|
|
if err := L.DoString(luaExpr); err != nil {
|
|
processJsonLogger.Error("Lua script execution failed: %v\nScript: %s", err, utils.LimitString(luaExpr, 200))
|
|
return commands, fmt.Errorf("lua script execution failed: %v", err)
|
|
}
|
|
processJsonLogger.Debug("Lua script executed successfully")
|
|
|
|
// Check if modification flag is set
|
|
modifiedVal := L.GetGlobal("modified")
|
|
if modifiedVal.Type() != lua.LTBool || !lua.LVAsBool(modifiedVal) {
|
|
processJsonLogger.Debug("Skipping - no modifications indicated by Lua script")
|
|
return commands, nil
|
|
}
|
|
|
|
// Get the modified data from Lua
|
|
modifiedData := L.GetGlobal("data")
|
|
if modifiedData.Type() != lua.LTTable {
|
|
processJsonLogger.Error("Expected 'data' to be a table after Lua processing, got %s", modifiedData.Type().String())
|
|
return commands, fmt.Errorf("expected 'data' to be a table after Lua processing")
|
|
}
|
|
|
|
// Convert back to Go interface
|
|
goData, err := FromLua(L, modifiedData)
|
|
if err != nil {
|
|
processJsonLogger.Error("Failed to convert Lua table back to Go: %v", err)
|
|
return commands, fmt.Errorf("failed to convert Lua table back to Go: %v", err)
|
|
}
|
|
|
|
commands, err = applyJSONChanges(content, jsonData, goData)
|
|
if err != nil {
|
|
processJsonLogger.Error("Failed to apply JSON changes: %v", err)
|
|
return commands, fmt.Errorf("failed to apply JSON changes: %v", err)
|
|
}
|
|
|
|
processJsonLogger.Debug("Total JSON processing time: %v", time.Since(startTime))
|
|
processJsonLogger.Debug("Generated %d total modifications", len(commands))
|
|
return commands, nil
|
|
}
|
|
|
|
func applyJSONChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) {
|
|
var commands []utils.ReplaceCommand
|
|
|
|
// Convert both to JSON for comparison
|
|
originalJSON, err := json.Marshal(originalData)
|
|
if err != nil {
|
|
return commands, fmt.Errorf("failed to marshal original data: %v", err)
|
|
}
|
|
|
|
modifiedJSON, err := json.Marshal(modifiedData)
|
|
if err != nil {
|
|
return commands, fmt.Errorf("failed to marshal modified data: %v", err)
|
|
}
|
|
|
|
// If no changes, return empty commands
|
|
if string(originalJSON) == string(modifiedJSON) {
|
|
return commands, nil
|
|
}
|
|
|
|
appliedCommands, err := applyChanges(content, originalData, modifiedData)
|
|
if err == nil && len(appliedCommands) > 0 {
|
|
return appliedCommands, nil
|
|
}
|
|
|
|
return commands, fmt.Errorf("failed to make any changes to the json")
|
|
}
|
|
|
|
// applyChanges attempts to make surgical changes while preserving exact formatting
|
|
func applyChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) {
|
|
var commands []utils.ReplaceCommand
|
|
|
|
// Find all changes between original and modified data
|
|
changes := findDeepChanges("", originalData, modifiedData)
|
|
|
|
fmt.Printf("DEBUG: Found %d changes: %v\n", len(changes), changes)
|
|
|
|
if len(changes) == 0 {
|
|
return commands, nil
|
|
}
|
|
|
|
// Sort removal operations by index in descending order to avoid index shifting
|
|
var removals []string
|
|
var valueChanges []string
|
|
|
|
for path := range changes {
|
|
if strings.HasSuffix(path, "@remove") {
|
|
removals = append(removals, path)
|
|
} else {
|
|
valueChanges = append(valueChanges, path)
|
|
}
|
|
}
|
|
|
|
// Sort removals by index (descending) to process from end to beginning
|
|
sort.Slice(removals, func(i, j int) bool {
|
|
// Extract index from path like "Rows.0.Inputs.1@remove"
|
|
indexI := extractIndexFromRemovalPath(removals[i])
|
|
indexJ := extractIndexFromRemovalPath(removals[j])
|
|
return indexI > indexJ // Descending order
|
|
})
|
|
|
|
// Apply removals first (from end to beginning to avoid index shifting)
|
|
for _, removalPath := range removals {
|
|
actualPath := strings.TrimSuffix(removalPath, "@remove")
|
|
index := extractIndexFromRemovalPath(removalPath)
|
|
arrayPath := getArrayPathFromElementPath(actualPath)
|
|
|
|
// Get the array element to remove
|
|
result := gjson.Get(content, actualPath)
|
|
if !result.Exists() {
|
|
continue
|
|
}
|
|
|
|
// Find the exact byte range to remove (including comma/formatting)
|
|
startPos, endPos := findArrayElementRemovalRange(content, arrayPath, index)
|
|
if startPos >= 0 && endPos > startPos {
|
|
commands = append(commands, utils.ReplaceCommand{
|
|
From: startPos,
|
|
To: endPos,
|
|
With: "", // Remove the element
|
|
})
|
|
}
|
|
}
|
|
|
|
// Apply value changes
|
|
for _, path := range valueChanges {
|
|
newValue := changes[path]
|
|
|
|
// Get the current value and its position in the original JSON
|
|
result := gjson.Get(content, path)
|
|
if !result.Exists() {
|
|
continue // Skip if path doesn't exist
|
|
}
|
|
|
|
// Get the exact byte positions of this value
|
|
startPos := result.Index
|
|
endPos := startPos + len(result.Raw)
|
|
|
|
// Convert the new value to JSON string WITHOUT using json.Marshal
|
|
var newValueStr string
|
|
switch v := newValue.(type) {
|
|
case string:
|
|
newValueStr = `"` + strings.ReplaceAll(v, `"`, `\"`) + `"`
|
|
case float64:
|
|
if v == float64(int64(v)) {
|
|
newValueStr = strconv.FormatInt(int64(v), 10)
|
|
} else {
|
|
newValueStr = strconv.FormatFloat(v, 'f', -1, 64)
|
|
}
|
|
case bool:
|
|
newValueStr = strconv.FormatBool(v)
|
|
case nil:
|
|
newValueStr = "null"
|
|
default:
|
|
// For complex types, we need to avoid json.Marshal
|
|
// This should not happen if we're doing true surgical edits
|
|
continue
|
|
}
|
|
|
|
// Create a replacement command for this specific value
|
|
commands = append(commands, utils.ReplaceCommand{
|
|
From: int(startPos),
|
|
To: int(endPos),
|
|
With: newValueStr,
|
|
})
|
|
}
|
|
|
|
return commands, nil
|
|
}
|
|
|
|
// extractIndexFromRemovalPath extracts the array index from a removal path like "Rows.0.Inputs.1@remove"
|
|
func extractIndexFromRemovalPath(path string) int {
|
|
parts := strings.Split(strings.TrimSuffix(path, "@remove"), ".")
|
|
if len(parts) > 0 {
|
|
lastPart := parts[len(parts)-1]
|
|
if index, err := strconv.Atoi(lastPart); err == nil {
|
|
return index
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// getArrayPathFromElementPath converts "Rows.0.Inputs.1" to "Rows.0.Inputs"
|
|
func getArrayPathFromElementPath(elementPath string) string {
|
|
parts := strings.Split(elementPath, ".")
|
|
if len(parts) > 0 {
|
|
return strings.Join(parts[:len(parts)-1], ".")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// findArrayElementRemovalRange finds the exact byte range to remove for an array element
|
|
func findArrayElementRemovalRange(content, arrayPath string, elementIndex int) (int, int) {
|
|
// Get the array using gjson
|
|
arrayResult := gjson.Get(content, arrayPath)
|
|
if !arrayResult.Exists() || !arrayResult.IsArray() {
|
|
return -1, -1
|
|
}
|
|
|
|
// Get all array elements
|
|
elements := arrayResult.Array()
|
|
if elementIndex >= len(elements) {
|
|
return -1, -1
|
|
}
|
|
|
|
// Get the target element
|
|
elementResult := elements[elementIndex]
|
|
startPos := int(elementResult.Index)
|
|
endPos := int(elementResult.Index + len(elementResult.Raw))
|
|
|
|
// Handle comma removal properly
|
|
if elementIndex == 0 && len(elements) > 1 {
|
|
// First element but not the only one - remove comma after
|
|
for i := endPos; i < len(content) && i < endPos+50; i++ {
|
|
if content[i] == ',' {
|
|
endPos = i + 1
|
|
break
|
|
}
|
|
}
|
|
} else if elementIndex == len(elements)-1 && len(elements) > 1 {
|
|
// Last element and not the only one - remove comma before
|
|
prevElementEnd := int(elements[elementIndex-1].Index + len(elements[elementIndex-1].Raw))
|
|
for i := prevElementEnd; i < startPos && i < len(content); i++ {
|
|
if content[i] == ',' {
|
|
startPos = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// If it's the only element, don't remove any commas
|
|
|
|
return startPos, endPos
|
|
}
|
|
|
|
// findDeepChanges recursively finds all paths that need to be changed
|
|
func findDeepChanges(basePath string, original, modified interface{}) map[string]interface{} {
|
|
changes := make(map[string]interface{})
|
|
|
|
switch orig := original.(type) {
|
|
case map[string]interface{}:
|
|
if mod, ok := modified.(map[string]interface{}); ok {
|
|
for key, modValue := range mod {
|
|
var currentPath string
|
|
if basePath == "" {
|
|
currentPath = key
|
|
} else {
|
|
currentPath = basePath + "." + key
|
|
}
|
|
|
|
if origValue, exists := orig[key]; exists {
|
|
// Key exists in both, check if value changed
|
|
switch modValue.(type) {
|
|
case map[string]interface{}, []interface{}:
|
|
// Recursively check nested structures
|
|
nestedChanges := findDeepChanges(currentPath, origValue, modValue)
|
|
for nestedPath, nestedValue := range nestedChanges {
|
|
changes[nestedPath] = nestedValue
|
|
}
|
|
default:
|
|
// Primitive value - check if changed
|
|
if !deepEqual(origValue, modValue) {
|
|
changes[currentPath] = modValue
|
|
}
|
|
}
|
|
} else {
|
|
// New key added - handle as structural change (skip for now)
|
|
// This is complex for surgical editing as it requires inserting into object
|
|
}
|
|
}
|
|
}
|
|
case []interface{}:
|
|
if mod, ok := modified.([]interface{}); ok {
|
|
// Handle array changes by detecting specific element operations
|
|
if len(orig) != len(mod) {
|
|
// Array length changed - detect if it's element removal
|
|
if len(orig) > len(mod) {
|
|
// Element(s) removed - find which ones by comparing content
|
|
removedIndices := findRemovedArrayElements(orig, mod)
|
|
for _, removedIndex := range removedIndices {
|
|
var currentPath string
|
|
if basePath == "" {
|
|
currentPath = fmt.Sprintf("%d@remove", removedIndex)
|
|
} else {
|
|
currentPath = fmt.Sprintf("%s.%d@remove", basePath, removedIndex)
|
|
}
|
|
changes[currentPath] = nil // Mark for removal
|
|
}
|
|
} else {
|
|
// Elements added - more complex, skip for now
|
|
}
|
|
} else {
|
|
// Same length - check individual elements for value changes
|
|
for i, modValue := range mod {
|
|
var currentPath string
|
|
if basePath == "" {
|
|
currentPath = strconv.Itoa(i)
|
|
} else {
|
|
currentPath = basePath + "." + strconv.Itoa(i)
|
|
}
|
|
|
|
if i < len(orig) {
|
|
// Index exists in both, check if value changed
|
|
switch modValue.(type) {
|
|
case map[string]interface{}, []interface{}:
|
|
// Recursively check nested structures
|
|
nestedChanges := findDeepChanges(currentPath, orig[i], modValue)
|
|
for nestedPath, nestedValue := range nestedChanges {
|
|
changes[nestedPath] = nestedValue
|
|
}
|
|
default:
|
|
// Primitive value - check if changed
|
|
if !deepEqual(orig[i], modValue) {
|
|
changes[currentPath] = modValue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
// For primitive types, compare directly
|
|
if !deepEqual(original, modified) {
|
|
if basePath == "" {
|
|
changes[""] = modified
|
|
} else {
|
|
changes[basePath] = modified
|
|
}
|
|
}
|
|
}
|
|
|
|
return changes
|
|
}
|
|
|
|
// findRemovedArrayElements compares two arrays and returns indices of removed elements
|
|
func findRemovedArrayElements(original, modified []interface{}) []int {
|
|
var removedIndices []int
|
|
|
|
// Simple approach: find elements in original that don't exist in modified
|
|
for i, origElement := range original {
|
|
found := false
|
|
for _, modElement := range modified {
|
|
if deepEqual(origElement, modElement) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
removedIndices = append(removedIndices, i)
|
|
}
|
|
}
|
|
|
|
return removedIndices
|
|
}
|
|
|
|
// deepEqual performs deep comparison of two values
|
|
func deepEqual(a, b interface{}) bool {
|
|
if a == nil && b == nil {
|
|
return true
|
|
}
|
|
if a == nil || b == nil {
|
|
return false
|
|
}
|
|
|
|
switch av := a.(type) {
|
|
case map[string]interface{}:
|
|
if bv, ok := b.(map[string]interface{}); ok {
|
|
if len(av) != len(bv) {
|
|
return false
|
|
}
|
|
for k, v := range av {
|
|
if !deepEqual(v, bv[k]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
case []interface{}:
|
|
if bv, ok := b.([]interface{}); ok {
|
|
if len(av) != len(bv) {
|
|
return false
|
|
}
|
|
for i, v := range av {
|
|
if !deepEqual(v, bv[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
default:
|
|
return a == b
|
|
}
|
|
}
|
|
|
|
// ToLuaTable converts a Go interface{} to a Lua table recursively
|
|
func ToLuaTable(L *lua.LState, data interface{}) (*lua.LTable, error) {
|
|
toLuaTableLogger := jsonLogger.WithPrefix("ToLuaTable")
|
|
toLuaTableLogger.Debug("Converting Go interface to Lua table")
|
|
toLuaTableLogger.Trace("Input data type: %T", data)
|
|
|
|
switch v := data.(type) {
|
|
case map[string]interface{}:
|
|
toLuaTableLogger.Debug("Converting map to Lua table")
|
|
table := L.CreateTable(0, len(v))
|
|
for key, value := range v {
|
|
luaValue, err := ToLuaValue(L, value)
|
|
if err != nil {
|
|
toLuaTableLogger.Error("Failed to convert map value for key %q: %v", key, err)
|
|
return nil, err
|
|
}
|
|
table.RawSetString(key, luaValue)
|
|
}
|
|
return table, nil
|
|
|
|
case []interface{}:
|
|
toLuaTableLogger.Debug("Converting slice to Lua table")
|
|
table := L.CreateTable(len(v), 0)
|
|
for i, value := range v {
|
|
luaValue, err := ToLuaValue(L, value)
|
|
if err != nil {
|
|
toLuaTableLogger.Error("Failed to convert slice value at index %d: %v", i, err)
|
|
return nil, err
|
|
}
|
|
table.RawSetInt(i+1, luaValue) // Lua arrays are 1-indexed
|
|
}
|
|
return table, nil
|
|
|
|
case string:
|
|
toLuaTableLogger.Debug("Converting string to Lua string")
|
|
return nil, fmt.Errorf("expected table or array, got string")
|
|
|
|
case float64:
|
|
toLuaTableLogger.Debug("Converting float64 to Lua number")
|
|
return nil, fmt.Errorf("expected table or array, got number")
|
|
|
|
case bool:
|
|
toLuaTableLogger.Debug("Converting bool to Lua boolean")
|
|
return nil, fmt.Errorf("expected table or array, got boolean")
|
|
|
|
case nil:
|
|
toLuaTableLogger.Debug("Converting nil to Lua nil")
|
|
return nil, fmt.Errorf("expected table or array, got nil")
|
|
|
|
default:
|
|
toLuaTableLogger.Error("Unsupported type for Lua table conversion: %T", v)
|
|
return nil, fmt.Errorf("unsupported type for Lua table conversion: %T", v)
|
|
}
|
|
}
|
|
|
|
// ToLuaValue converts a Go interface{} to a Lua value
|
|
func ToLuaValue(L *lua.LState, data interface{}) (lua.LValue, error) {
|
|
toLuaValueLogger := jsonLogger.WithPrefix("ToLuaValue")
|
|
toLuaValueLogger.Debug("Converting Go interface to Lua value")
|
|
toLuaValueLogger.Trace("Input data type: %T", data)
|
|
|
|
switch v := data.(type) {
|
|
case map[string]interface{}:
|
|
toLuaValueLogger.Debug("Converting map to Lua table")
|
|
table := L.CreateTable(0, len(v))
|
|
for key, value := range v {
|
|
luaValue, err := ToLuaValue(L, value)
|
|
if err != nil {
|
|
toLuaValueLogger.Error("Failed to convert map value for key %q: %v", key, err)
|
|
return lua.LNil, err
|
|
}
|
|
table.RawSetString(key, luaValue)
|
|
}
|
|
return table, nil
|
|
|
|
case []interface{}:
|
|
toLuaValueLogger.Debug("Converting slice to Lua table")
|
|
table := L.CreateTable(len(v), 0)
|
|
for i, value := range v {
|
|
luaValue, err := ToLuaValue(L, value)
|
|
if err != nil {
|
|
toLuaValueLogger.Error("Failed to convert slice value at index %d: %v", i, err)
|
|
return lua.LNil, err
|
|
}
|
|
table.RawSetInt(i+1, luaValue) // Lua arrays are 1-indexed
|
|
}
|
|
return table, nil
|
|
|
|
case string:
|
|
toLuaValueLogger.Debug("Converting string to Lua string")
|
|
return lua.LString(v), nil
|
|
|
|
case float64:
|
|
toLuaValueLogger.Debug("Converting float64 to Lua number")
|
|
return lua.LNumber(v), nil
|
|
|
|
case bool:
|
|
toLuaValueLogger.Debug("Converting bool to Lua boolean")
|
|
return lua.LBool(v), nil
|
|
|
|
case nil:
|
|
toLuaValueLogger.Debug("Converting nil to Lua nil")
|
|
return lua.LNil, nil
|
|
|
|
default:
|
|
toLuaValueLogger.Error("Unsupported type for Lua value conversion: %T", v)
|
|
return lua.LNil, fmt.Errorf("unsupported type for Lua value conversion: %T", v)
|
|
}
|
|
}
|