This commit is contained in:
2025-08-21 22:15:02 +02:00
parent bff7cc2a27
commit 1a8c0b9f90
4 changed files with 468 additions and 12 deletions

View File

@@ -7,6 +7,8 @@ import (
"time"
logger "git.site.quack-lab.dev/dave/cylogger"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
lua "github.com/yuin/gopher-lua"
)
@@ -85,26 +87,184 @@ func ProcessJSON(content string, command utils.ModifyCommand, filename string) (
return commands, fmt.Errorf("failed to convert Lua table back to Go: %v", err)
}
// Marshal back to JSON
modifiedJSON, err := json.MarshalIndent(goData, "", " ")
// Use surgical JSON editing instead of full replacement
commands, err = applySurgicalJSONChanges(content, jsonData, goData)
if err != nil {
processJsonLogger.Error("Failed to marshal modified data to JSON: %v", err)
return commands, fmt.Errorf("failed to marshal modified data to JSON: %v", err)
processJsonLogger.Error("Failed to apply surgical JSON changes: %v", err)
return commands, fmt.Errorf("failed to apply surgical JSON changes: %v", err)
}
// Create replacement command for the entire file
// For JSON mode, we always replace the entire content
commands = append(commands, utils.ReplaceCommand{
From: 0,
To: len(content),
With: string(modifiedJSON),
})
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")