diff --git a/processor/json.go b/processor/json.go index 2f03b11..b205e90 100644 --- a/processor/json.go +++ b/processor/json.go @@ -7,7 +7,6 @@ import ( "time" logger "git.site.quack-lab.dev/dave/cylogger" - "github.com/tidwall/gjson" "github.com/tidwall/sjson" lua "github.com/yuin/gopher-lua" ) @@ -102,59 +101,56 @@ func ProcessJSON(content string, command utils.ModifyCommand, filename string) ( // 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) + + // 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 } -// applySurgicalChanges attempts to make surgical changes using gjson and sjson -func applySurgicalChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) { +// applyTrueSurgicalChanges attempts to make surgical changes while preserving exact formatting +func applyTrueSurgicalChanges(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) - + changes := findDeepChanges("", originalData, modifiedData) + if len(changes) == 0 { return commands, nil } - - // Apply changes surgically + + // Apply changes surgically using sjson.Set() to preserve formatting modifiedContent := content for path, newValue := range changes { var err error @@ -163,64 +159,99 @@ func applySurgicalChanges(content string, originalData, modifiedData interface{} 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), + With: modifiedContent, }) } - + return commands, nil } -// findSurgicalChanges finds specific paths that need to be changed -func findSurgicalChanges(result gjson.Result, original, modified interface{}) map[string]interface{} { +// 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) { - changes[key] = 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[key] = modValue + changes[currentPath] = 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 + // 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) { - changes[""] = modified + if basePath == "" { + changes[""] = modified + } else { + changes[basePath] = modified + } } } - + return changes } diff --git a/test_surgical.yml b/test_surgical.yml new file mode 100644 index 0000000..5ea9fd4 --- /dev/null +++ b/test_surgical.yml @@ -0,0 +1,11 @@ +- name: SurgicalWeightTest + json: true + lua: | + -- This demonstrates surgical JSON editing + -- Only the Weight field of Item_Fiber will be modified + data.Rows[1].Weight = 999 + modified = true + files: + - 'D_Itemable.json' + reset: false + loglevel: INFO \ No newline at end of file