package processor import ( "cook/utils" "encoding/json" "fmt" "time" logger "git.site.quack-lab.dev/dave/cylogger" "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 true surgical approach that preserves formatting surgicalCommands, err := applyTrueSurgicalChanges(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 } // applyTrueSurgicalChanges attempts to make surgical changes while preserving exact formatting func applyTrueSurgicalChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) { var commands []utils.ReplaceCommand // Find changes by comparing the data structures changes := findDeepChanges("", originalData, modifiedData) if len(changes) == 0 { return commands, nil } // Apply changes surgically using sjson.Set() to preserve formatting 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 if modifiedContent != content { commands = append(commands, utils.ReplaceCommand{ From: 0, To: len(content), With: modifiedContent, }) } return commands, nil } // 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 { // Check each key in the modified data 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 if !deepEqual(origValue, modValue) { // If it's a nested object/array, recurse switch modValue.(type) { case map[string]interface{}, []interface{}: nestedChanges := findDeepChanges(currentPath, origValue, modValue) for nestedPath, nestedValue := range nestedChanges { changes[nestedPath] = nestedValue } default: // Primitive value changed changes[currentPath] = modValue } } } else { // New key added changes[currentPath] = modValue } } } case []interface{}: if mod, ok := modified.([]interface{}); ok { // For arrays, check each index for i, modValue := range mod { var currentPath string if basePath == "" { currentPath = fmt.Sprintf("%d", i) } else { currentPath = fmt.Sprintf("%s.%d", basePath, i) } if i < len(orig) { // Index exists in both, check if value changed if !deepEqual(orig[i], modValue) { // If it's a nested object/array, recurse switch modValue.(type) { case map[string]interface{}, []interface{}: nestedChanges := findDeepChanges(currentPath, orig[i], modValue) for nestedPath, nestedValue := range nestedChanges { changes[nestedPath] = nestedValue } default: // Primitive value changed changes[currentPath] = modValue } } } else { // New array element added changes[currentPath] = modValue } } } default: // For primitive types, compare directly if !deepEqual(original, modified) { if basePath == "" { changes[""] = modified } else { changes[basePath] = 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) } }