377 lines
12 KiB
Go
377 lines
12 KiB
Go
package processor
|
|
|
|
import (
|
|
"cook/utils"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
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)
|
|
}
|
|
|
|
// Use surgical JSON editing instead of full replacement
|
|
commands, err = applySurgicalJSONChanges(content, jsonData, goData)
|
|
if err != nil {
|
|
processJsonLogger.Error("Failed to apply surgical JSON changes: %v", err)
|
|
return commands, fmt.Errorf("failed to apply surgical 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
|
|
}
|
|
|
|
// applySurgicalJSONChanges compares original and modified data and applies changes surgically
|
|
func applySurgicalJSONChanges(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
|
|
}
|
|
|
|
// Try surgical approach first
|
|
surgicalCommands, err := applySurgicalChanges(content, originalData, modifiedData)
|
|
if err == nil && len(surgicalCommands) > 0 {
|
|
return surgicalCommands, nil
|
|
}
|
|
|
|
// Fall back to full replacement with proper formatting
|
|
modifiedJSONIndented, err := json.MarshalIndent(modifiedData, "", " ")
|
|
if err != nil {
|
|
return commands, fmt.Errorf("failed to marshal modified data with indentation: %v", err)
|
|
}
|
|
|
|
commands = append(commands, utils.ReplaceCommand{
|
|
From: 0,
|
|
To: len(content),
|
|
With: string(modifiedJSONIndented),
|
|
})
|
|
|
|
return commands, nil
|
|
}
|
|
|
|
// applySurgicalChanges attempts to make surgical changes using gjson and sjson
|
|
func applySurgicalChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) {
|
|
var commands []utils.ReplaceCommand
|
|
|
|
// Parse the original content with gjson to get the structure
|
|
result := gjson.Parse(content)
|
|
|
|
// Find changes by comparing the data structures
|
|
changes := findSurgicalChanges(result, originalData, modifiedData)
|
|
|
|
if len(changes) == 0 {
|
|
return commands, nil
|
|
}
|
|
|
|
// Apply changes surgically
|
|
modifiedContent := content
|
|
for path, newValue := range changes {
|
|
var err error
|
|
modifiedContent, err = sjson.Set(modifiedContent, path, newValue)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to apply surgical change at path %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
// If we successfully made changes, create a replacement command
|
|
// But ensure we preserve formatting by using json.MarshalIndent
|
|
if modifiedContent != content {
|
|
// Parse the surgically modified content and re-format it
|
|
var parsedData interface{}
|
|
if err := json.Unmarshal([]byte(modifiedContent), &parsedData); err != nil {
|
|
return nil, fmt.Errorf("failed to parse surgically modified content: %v", err)
|
|
}
|
|
|
|
formattedContent, err := json.MarshalIndent(parsedData, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to format surgically modified content: %v", err)
|
|
}
|
|
|
|
commands = append(commands, utils.ReplaceCommand{
|
|
From: 0,
|
|
To: len(content),
|
|
With: string(formattedContent),
|
|
})
|
|
}
|
|
|
|
return commands, nil
|
|
}
|
|
|
|
// findSurgicalChanges finds specific paths that need to be changed
|
|
func findSurgicalChanges(result gjson.Result, 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 {
|
|
if origValue, exists := orig[key]; exists {
|
|
// Key exists in both, check if value changed
|
|
if !deepEqual(origValue, modValue) {
|
|
changes[key] = modValue
|
|
}
|
|
} else {
|
|
// New key added
|
|
changes[key] = modValue
|
|
}
|
|
}
|
|
}
|
|
case []interface{}:
|
|
if mod, ok := modified.([]interface{}); ok {
|
|
// For arrays, we'll do a simple replacement for now
|
|
if !deepEqual(orig, mod) {
|
|
changes[""] = mod // Root path for array replacement
|
|
}
|
|
}
|
|
default:
|
|
// For primitive types, compare directly
|
|
if !deepEqual(original, modified) {
|
|
changes[""] = modified
|
|
}
|
|
}
|
|
|
|
return changes
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|