From 779d1e0a0ed4f90f3219bd1406cda75c641a02e5 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Thu, 21 Aug 2025 23:16:23 +0200 Subject: [PATCH] Fix some more shit I guess --- processor/json.go | 198 +++++++++++++++++++++----------- processor/surgical_json_test.go | 6 +- 2 files changed, 135 insertions(+), 69 deletions(-) diff --git a/processor/json.go b/processor/json.go index c47f014..4b4e9c1 100644 --- a/processor/json.go +++ b/processor/json.go @@ -89,7 +89,7 @@ func ProcessJSON(content string, command utils.ModifyCommand, filename string) ( return commands, fmt.Errorf("failed to convert Lua table back to Go: %v", err) } - commands, err = applyJSONChanges(content, jsonData, goData) + commands, err = applySurgicalJSONChanges(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) @@ -100,66 +100,49 @@ func ProcessJSON(content string, command utils.ModifyCommand, filename string) ( return commands, nil } -func applyJSONChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) { +// 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 - } - + + // Apply surgical changes 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 additions []string var valueChanges []string - + for path := range changes { if strings.HasSuffix(path, "@remove") { removals = append(removals, path) + } else if strings.HasSuffix(path, "@add") { + additions = append(additions, 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 - }) - + + jsonLogger.Info("applyChanges: Found %d changes: %v", len(changes), changes) + + jsonLogger.Info("applyChanges: %d removals, %d additions, %d value changes", len(removals), len(additions), len(valueChanges)) + // Apply removals first (from end to beginning to avoid index shifting) for _, removalPath := range removals { actualPath := strings.TrimSuffix(removalPath, "@remove") @@ -182,50 +165,97 @@ func applyChanges(content string, originalData, modifiedData interface{}) ([]uti }) } } - - // Apply value changes + + // Apply additions (new fields) + for _, additionPath := range additions { + actualPath := strings.TrimSuffix(additionPath, "@add") + newValue := changes[additionPath] + + jsonLogger.Info("Processing addition: path=%s, value=%v", actualPath, newValue) + + // Find the parent object to add the field to + parentPath := getParentPath(actualPath) + fieldName := getFieldName(actualPath) + + jsonLogger.Info("Parent path: %s, field name: %s", parentPath, fieldName) + + // Get the parent object + var parentResult gjson.Result + if parentPath == "" { + // Adding to root object - get the entire JSON + parentResult = gjson.Parse(content) + } else { + parentResult = gjson.Get(content, parentPath) + } + + if !parentResult.Exists() { + jsonLogger.Info("Parent path %s does not exist, skipping", parentPath) + continue + } + + // Find where to insert the new field (at the end of the object) + startPos := int(parentResult.Index + len(parentResult.Raw) - 1) // Before closing brace + + jsonLogger.Info("Inserting at pos %d", startPos) + + // Convert the new value to JSON string + newValueStr := convertValueToJSONString(newValue) + + // Insert the new field + insertText := fmt.Sprintf(`,"%s":%s`, fieldName, newValueStr) + + jsonLogger.Info("Inserting text: %q", insertText) + + commands = append(commands, utils.ReplaceCommand{ + From: startPos, + To: startPos, + With: insertText, + }) + + jsonLogger.Info("Added addition command: From=%d, To=%d, With=%q", startPos, startPos, insertText) + } + + // Apply value changes (in reverse order to avoid position shifting) + sort.Slice(valueChanges, func(i, j int) bool { + // Get positions for comparison + resultI := gjson.Get(content, valueChanges[i]) + resultJ := gjson.Get(content, valueChanges[j]) + return resultI.Index > resultJ.Index // Descending order + }) + for _, path := range valueChanges { newValue := changes[path] - + + jsonLogger.Info("Processing value change: path=%s, value=%v", path, newValue) + // Get the current value and its position in the original JSON result := gjson.Get(content, path) if !result.Exists() { + jsonLogger.Info("Path %s does not exist, skipping", path) 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 - } - + + jsonLogger.Info("Found value at pos %d-%d: %q", startPos, endPos, result.Raw) + + // Convert the new value to JSON string + newValueStr := convertValueToJSONString(newValue) + + jsonLogger.Info("Converting to: %q", newValueStr) + // Create a replacement command for this specific value commands = append(commands, utils.ReplaceCommand{ From: int(startPos), To: int(endPos), With: newValueStr, }) + + jsonLogger.Info("Added command: From=%d, To=%d, With=%q", int(startPos), int(endPos), newValueStr) } - + return commands, nil } @@ -250,6 +280,45 @@ func getArrayPathFromElementPath(elementPath string) string { return "" } +// getParentPath extracts the parent path from a full path like "Rows.0.Inputs.1" +func getParentPath(fullPath string) string { + parts := strings.Split(fullPath, ".") + if len(parts) > 0 { + return strings.Join(parts[:len(parts)-1], ".") + } + return "" +} + +// getFieldName extracts the field name from a full path like "Rows.0.Inputs.1" +func getFieldName(fullPath string) string { + parts := strings.Split(fullPath, ".") + if len(parts) > 0 { + return parts[len(parts)-1] + } + return "" +} + +// convertValueToJSONString converts a Go interface{} to a JSON string representation +func convertValueToJSONString(value interface{}) string { + switch v := value.(type) { + case string: + return `"` + strings.ReplaceAll(v, `"`, `\"`) + `"` + case float64: + if v == float64(int64(v)) { + return strconv.FormatInt(int64(v), 10) + } + return strconv.FormatFloat(v, 'f', -1, 64) + case bool: + return strconv.FormatBool(v) + case nil: + return "null" + default: + // For complex types, we need to avoid json.Marshal + // This should not happen if we're doing true surgical edits + 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 @@ -300,6 +369,7 @@ func findDeepChanges(basePath string, original, modified interface{}) map[string switch orig := original.(type) { case map[string]interface{}: if mod, ok := modified.(map[string]interface{}); ok { + // Check for new keys added in modified data for key, modValue := range mod { var currentPath string if basePath == "" { @@ -307,7 +377,7 @@ func findDeepChanges(basePath string, original, modified interface{}) map[string } else { currentPath = basePath + "." + key } - + if origValue, exists := orig[key]; exists { // Key exists in both, check if value changed switch modValue.(type) { @@ -324,8 +394,8 @@ func findDeepChanges(basePath string, original, modified interface{}) map[string } } } else { - // New key added - handle as structural change (skip for now) - // This is complex for surgical editing as it requires inserting into object + // New key added - mark for addition + changes[currentPath+"@add"] = modValue } } } diff --git a/processor/surgical_json_test.go b/processor/surgical_json_test.go index bd04ac4..a3732f0 100644 --- a/processor/surgical_json_test.go +++ b/processor/surgical_json_test.go @@ -11,7 +11,6 @@ func TestSurgicalJSONEditing(t *testing.T) { content string luaCode string expected string - skip bool }{ { name: "Modify single field", @@ -43,7 +42,7 @@ modified = true expected: `{ "name": "test", "value": 42 -,"newField":"added"}`, +,"newField":"added"}`, // sjson.Set() adds new fields in compact format }, { name: "Modify nested field", @@ -73,9 +72,6 @@ modified = true for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.skip { - t.Skip("Skipping test due to surgical approach not handling this case yet") - } command := utils.ModifyCommand{ Name: "test", Lua: tt.luaCode,