Compare commits
	
		
			2 Commits
		
	
	
		
			v6.4.4
			...
			fd1df6e40e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fd1df6e40e | |||
| 1a8c0b9f90 | 
							
								
								
									
										13
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							@@ -98,19 +98,6 @@
 | 
				
			|||||||
			"args": [
 | 
								"args": [
 | 
				
			||||||
				"cook_tacz.yml",
 | 
									"cook_tacz.yml",
 | 
				
			||||||
			]
 | 
								]
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"name": "Launch Package (ICARUS)",
 | 
					 | 
				
			||||||
			"type": "go",
 | 
					 | 
				
			||||||
			"request": "launch",
 | 
					 | 
				
			||||||
			"mode": "auto",
 | 
					 | 
				
			||||||
			"program": "${workspaceFolder}",
 | 
					 | 
				
			||||||
			"cwd": "C:/Users/Administrator/Seafile/Games-ICARUS/Icarus/Saved/IME3/Mods",
 | 
					 | 
				
			||||||
			"args": [
 | 
					 | 
				
			||||||
				"-loglevel",
 | 
					 | 
				
			||||||
				"trace",
 | 
					 | 
				
			||||||
				"cook_processorrecipes.yml",
 | 
					 | 
				
			||||||
			]
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	]
 | 
						]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								go.mod
									
									
									
									
									
								
							@@ -13,6 +13,7 @@ require (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
	github.com/davecgh/go-spew v1.1.1 // indirect
 | 
						github.com/davecgh/go-spew v1.1.1 // indirect
 | 
				
			||||||
 | 
						github.com/google/go-cmp v0.6.0 // indirect
 | 
				
			||||||
	github.com/hexops/valast v1.5.0 // indirect
 | 
						github.com/hexops/valast v1.5.0 // indirect
 | 
				
			||||||
	github.com/jinzhu/inflection v1.0.0 // indirect
 | 
						github.com/jinzhu/inflection v1.0.0 // indirect
 | 
				
			||||||
	github.com/jinzhu/now v1.1.5 // indirect
 | 
						github.com/jinzhu/now v1.1.5 // indirect
 | 
				
			||||||
@@ -20,8 +21,10 @@ require (
 | 
				
			|||||||
	github.com/mattn/go-sqlite3 v1.14.22 // indirect
 | 
						github.com/mattn/go-sqlite3 v1.14.22 // indirect
 | 
				
			||||||
	github.com/pmezard/go-difflib v1.0.0 // indirect
 | 
						github.com/pmezard/go-difflib v1.0.0 // indirect
 | 
				
			||||||
	github.com/rogpeppe/go-internal v1.14.1 // indirect
 | 
						github.com/rogpeppe/go-internal v1.14.1 // indirect
 | 
				
			||||||
 | 
						github.com/tidwall/gjson v1.18.0 // indirect
 | 
				
			||||||
	github.com/tidwall/match v1.1.1 // indirect
 | 
						github.com/tidwall/match v1.1.1 // indirect
 | 
				
			||||||
	github.com/tidwall/pretty v1.2.0 // indirect
 | 
						github.com/tidwall/pretty v1.2.0 // indirect
 | 
				
			||||||
 | 
						github.com/tidwall/sjson v1.2.5 // indirect
 | 
				
			||||||
	golang.org/x/mod v0.21.0 // indirect
 | 
						golang.org/x/mod v0.21.0 // indirect
 | 
				
			||||||
	golang.org/x/sync v0.11.0 // indirect
 | 
						golang.org/x/sync v0.11.0 // indirect
 | 
				
			||||||
	golang.org/x/text v0.22.0 // indirect
 | 
						golang.org/x/text v0.22.0 // indirect
 | 
				
			||||||
@@ -30,8 +33,4 @@ require (
 | 
				
			|||||||
	mvdan.cc/gofumpt v0.4.0 // indirect
 | 
						mvdan.cc/gofumpt v0.4.0 // indirect
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require gorm.io/driver/sqlite v1.6.0
 | 
				
			||||||
	github.com/google/go-cmp v0.6.0
 | 
					 | 
				
			||||||
	github.com/tidwall/gjson v1.18.0
 | 
					 | 
				
			||||||
	gorm.io/driver/sqlite v1.6.0
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.sum
									
									
									
									
									
								
							@@ -36,12 +36,15 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
 | 
				
			|||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 | 
					github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 | 
				
			||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 | 
					github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 | 
				
			||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 | 
					github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 | 
				
			||||||
 | 
					github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 | 
				
			||||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
 | 
					github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
 | 
				
			||||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 | 
					github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 | 
				
			||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
 | 
					github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
 | 
				
			||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
 | 
					github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
 | 
				
			||||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
 | 
					github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
 | 
				
			||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 | 
					github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 | 
				
			||||||
 | 
					github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
 | 
				
			||||||
 | 
					github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
 | 
				
			||||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
 | 
					github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
 | 
				
			||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
 | 
					github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
 | 
				
			||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
 | 
					golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										335
									
								
								isolate_test.go
									
									
									
									
									
								
							
							
						
						
									
										335
									
								
								isolate_test.go
									
									
									
									
									
								
							@@ -1,335 +0,0 @@
 | 
				
			|||||||
package main
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"path/filepath"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"cook/utils"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	logger "git.site.quack-lab.dev/dave/cylogger"
 | 
					 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestIsolateCommandsSequentialExecution(t *testing.T) {
 | 
					 | 
				
			||||||
	// Create a temporary directory for testing
 | 
					 | 
				
			||||||
	tmpDir, err := os.MkdirTemp("", "isolate-sequential-test")
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to create temp dir: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	defer os.RemoveAll(tmpDir)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create test file content
 | 
					 | 
				
			||||||
	testContent := `BEGIN
 | 
					 | 
				
			||||||
block1 content with value 42
 | 
					 | 
				
			||||||
END
 | 
					 | 
				
			||||||
Some other content
 | 
					 | 
				
			||||||
BEGIN
 | 
					 | 
				
			||||||
block2 content with value 100
 | 
					 | 
				
			||||||
END
 | 
					 | 
				
			||||||
More content
 | 
					 | 
				
			||||||
BEGIN
 | 
					 | 
				
			||||||
block3 content with value 200
 | 
					 | 
				
			||||||
END`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	testFile := filepath.Join(tmpDir, "test.txt")
 | 
					 | 
				
			||||||
	err = os.WriteFile(testFile, []byte(testContent), 0644)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to write test file: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Change to temp directory
 | 
					 | 
				
			||||||
	origDir, _ := os.Getwd()
 | 
					 | 
				
			||||||
	defer os.Chdir(origDir)
 | 
					 | 
				
			||||||
	os.Chdir(tmpDir)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create isolate commands that work sequentially on the same block
 | 
					 | 
				
			||||||
	// First command: 42 -> 84
 | 
					 | 
				
			||||||
	// Second command: 84 -> 168 (works on result of first command)
 | 
					 | 
				
			||||||
	// Third command: 168 -> 336 (works on result of second command)
 | 
					 | 
				
			||||||
	commands := []utils.ModifyCommand{
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			Name:    "MultiplyFirst",
 | 
					 | 
				
			||||||
			Regex:   `BEGIN\n(?P<block>.*?value 42.*?)\nEND`,
 | 
					 | 
				
			||||||
			Lua:     `replacement = "BEGIN\n" .. string.gsub(block, "42", "84") .. "\nEND"; return true`,
 | 
					 | 
				
			||||||
			Files:   []string{"test.txt"},
 | 
					 | 
				
			||||||
			Isolate: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			Name:    "MultiplySecond",
 | 
					 | 
				
			||||||
			Regex:   `BEGIN\nblock1 content with value (?P<value>!num)\nEND`,
 | 
					 | 
				
			||||||
			Lua:     `value = "168"; return true`,
 | 
					 | 
				
			||||||
			Files:   []string{"test.txt"},
 | 
					 | 
				
			||||||
			Isolate: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			Name:    "MultiplyThird",
 | 
					 | 
				
			||||||
			Regex:   `BEGIN\nblock1 content with value (?P<value>!num)\nEND`,
 | 
					 | 
				
			||||||
			Lua:     `value = "336"; return true`,
 | 
					 | 
				
			||||||
			Files:   []string{"test.txt"},
 | 
					 | 
				
			||||||
			Isolate: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Associate files with commands
 | 
					 | 
				
			||||||
	files := []string{"test.txt"}
 | 
					 | 
				
			||||||
	associations, err := utils.AssociateFilesWithCommands(files, commands)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to associate files with commands: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify that all three isolate commands are associated
 | 
					 | 
				
			||||||
	association := associations["test.txt"]
 | 
					 | 
				
			||||||
	assert.Len(t, association.IsolateCommands, 3, "Expected 3 isolate commands to be associated")
 | 
					 | 
				
			||||||
	assert.Len(t, association.Commands, 0, "Expected 0 regular commands")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Run the isolate commands
 | 
					 | 
				
			||||||
	result, err := RunIsolateCommands(association, "test.txt", testContent)
 | 
					 | 
				
			||||||
	if err != nil && err != NothingToDo {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to run isolate commands: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify that all isolate commands were applied sequentially
 | 
					 | 
				
			||||||
	// First command: 42 -> 84
 | 
					 | 
				
			||||||
	// Second command: 84 -> 168 (works on result of first)
 | 
					 | 
				
			||||||
	// Third command: 168 -> 336 (works on result of second)
 | 
					 | 
				
			||||||
	assert.Contains(t, result, "value 336", "Final result should be 336 after sequential processing")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify that intermediate and original values are no longer present
 | 
					 | 
				
			||||||
	assert.NotContains(t, result, "value 42", "Original value 42 should be replaced")
 | 
					 | 
				
			||||||
	assert.NotContains(t, result, "value 84", "Intermediate value 84 should be replaced")
 | 
					 | 
				
			||||||
	assert.NotContains(t, result, "value 168", "Intermediate value 168 should be replaced")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify other blocks remain unchanged
 | 
					 | 
				
			||||||
	assert.Contains(t, result, "value 100", "Second block should remain unchanged")
 | 
					 | 
				
			||||||
	assert.Contains(t, result, "value 200", "Third block should remain unchanged")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Logf("Original content:\n%s\n", testContent)
 | 
					 | 
				
			||||||
	t.Logf("Result content:\n%s\n", result)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestIsolateCommandsWithDifferentPatterns(t *testing.T) {
 | 
					 | 
				
			||||||
	// Create a temporary directory for testing
 | 
					 | 
				
			||||||
	tmpDir, err := os.MkdirTemp("", "isolate-different-patterns-test")
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to create temp dir: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	defer os.RemoveAll(tmpDir)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create test file content with distinct patterns
 | 
					 | 
				
			||||||
	testContent := `SECTION1
 | 
					 | 
				
			||||||
value = 10
 | 
					 | 
				
			||||||
END_SECTION1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SECTION2
 | 
					 | 
				
			||||||
value = 20
 | 
					 | 
				
			||||||
END_SECTION2`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	testFile := filepath.Join(tmpDir, "test.txt")
 | 
					 | 
				
			||||||
	err = os.WriteFile(testFile, []byte(testContent), 0644)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to write test file: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Change to temp directory
 | 
					 | 
				
			||||||
	origDir, _ := os.Getwd()
 | 
					 | 
				
			||||||
	defer os.Chdir(origDir)
 | 
					 | 
				
			||||||
	os.Chdir(tmpDir)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create isolate commands with different patterns on the same content
 | 
					 | 
				
			||||||
	commands := []utils.ModifyCommand{
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			Name:    "UpdateSection1",
 | 
					 | 
				
			||||||
			Regex:   `SECTION1.*?value = (?P<value>!num).*?END_SECTION1`,
 | 
					 | 
				
			||||||
			Lua:     `value = "100"; return true`,
 | 
					 | 
				
			||||||
			Files:   []string{"test.txt"},
 | 
					 | 
				
			||||||
			Isolate: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			Name:    "UpdateSection2",
 | 
					 | 
				
			||||||
			Regex:   `SECTION2.*?value = (?P<value>!num).*?END_SECTION2`,
 | 
					 | 
				
			||||||
			Lua:     `value = "200"; return true`,
 | 
					 | 
				
			||||||
			Files:   []string{"test.txt"},
 | 
					 | 
				
			||||||
			Isolate: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Associate files with commands
 | 
					 | 
				
			||||||
	files := []string{"test.txt"}
 | 
					 | 
				
			||||||
	associations, err := utils.AssociateFilesWithCommands(files, commands)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to associate files with commands: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Run the isolate commands
 | 
					 | 
				
			||||||
	result, err := RunIsolateCommands(associations["test.txt"], "test.txt", testContent)
 | 
					 | 
				
			||||||
	if err != nil && err != NothingToDo {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to run isolate commands: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify that both isolate commands were applied
 | 
					 | 
				
			||||||
	assert.Contains(t, result, "value = 100", "Section1 should be updated")
 | 
					 | 
				
			||||||
	assert.Contains(t, result, "value = 200", "Section2 should be updated")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify original values are gone (use exact matches)
 | 
					 | 
				
			||||||
	assert.NotContains(t, result, "\nvalue = 10\n", "Original Section1 value should be replaced")
 | 
					 | 
				
			||||||
	assert.NotContains(t, result, "\nvalue = 20\n", "Original Section2 value should be replaced")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Logf("Original content:\n%s\n", testContent)
 | 
					 | 
				
			||||||
	t.Logf("Result content:\n%s\n", result)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestIsolateCommandsWithJSONMode(t *testing.T) {
 | 
					 | 
				
			||||||
	// Create a temporary directory for testing
 | 
					 | 
				
			||||||
	tmpDir, err := os.MkdirTemp("", "isolate-json-test")
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to create temp dir: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	defer os.RemoveAll(tmpDir)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create test JSON content
 | 
					 | 
				
			||||||
	testContent := `{
 | 
					 | 
				
			||||||
  "section1": {
 | 
					 | 
				
			||||||
    "value": 42
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "section2": {
 | 
					 | 
				
			||||||
    "value": 100
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	testFile := filepath.Join(tmpDir, "test.json")
 | 
					 | 
				
			||||||
	err = os.WriteFile(testFile, []byte(testContent), 0644)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to write test file: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Change to temp directory
 | 
					 | 
				
			||||||
	origDir, _ := os.Getwd()
 | 
					 | 
				
			||||||
	defer os.Chdir(origDir)
 | 
					 | 
				
			||||||
	os.Chdir(tmpDir)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create isolate commands with JSON mode
 | 
					 | 
				
			||||||
	commands := []utils.ModifyCommand{
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			Name:    "UpdateJSONFirst",
 | 
					 | 
				
			||||||
			JSON:    true,
 | 
					 | 
				
			||||||
			Lua:     `data.section1.value = data.section1.value * 2; return true`,
 | 
					 | 
				
			||||||
			Files:   []string{"test.json"},
 | 
					 | 
				
			||||||
			Isolate: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			Name:    "UpdateJSONSecond",
 | 
					 | 
				
			||||||
			JSON:    true,
 | 
					 | 
				
			||||||
			Lua:     `data.section2.value = data.section2.value * 3; return true`,
 | 
					 | 
				
			||||||
			Files:   []string{"test.json"},
 | 
					 | 
				
			||||||
			Isolate: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Associate files with commands
 | 
					 | 
				
			||||||
	files := []string{"test.json"}
 | 
					 | 
				
			||||||
	associations, err := utils.AssociateFilesWithCommands(files, commands)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to associate files with commands: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Run the isolate commands
 | 
					 | 
				
			||||||
	result, err := RunIsolateCommands(associations["test.json"], "test.json", testContent)
 | 
					 | 
				
			||||||
	if err != nil && err != NothingToDo {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to run isolate commands: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify that both JSON isolate commands were applied
 | 
					 | 
				
			||||||
	assert.Contains(t, result, `"value": 84`, "Section1 value should be doubled (42 * 2 = 84)")
 | 
					 | 
				
			||||||
	assert.Contains(t, result, `"value": 300`, "Section2 value should be tripled (100 * 3 = 300)")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify original values are gone
 | 
					 | 
				
			||||||
	assert.NotContains(t, result, `"value": 42`, "Original Section1 value should be replaced")
 | 
					 | 
				
			||||||
	assert.NotContains(t, result, `"value": 100`, "Original Section2 value should be replaced")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Logf("Original content:\n%s\n", testContent)
 | 
					 | 
				
			||||||
	t.Logf("Result content:\n%s\n", result)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestIsolateVsRegularCommands(t *testing.T) {
 | 
					 | 
				
			||||||
	// Create a temporary directory for testing
 | 
					 | 
				
			||||||
	tmpDir, err := os.MkdirTemp("", "isolate-regular-test")
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to create temp dir: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	defer os.RemoveAll(tmpDir)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create test file with distinct sections
 | 
					 | 
				
			||||||
	testContent := `ISOLATE_SECTION
 | 
					 | 
				
			||||||
value = 5
 | 
					 | 
				
			||||||
END_ISOLATE
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
REGULAR_SECTION
 | 
					 | 
				
			||||||
value = 10
 | 
					 | 
				
			||||||
END_REGULAR`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	testFile := filepath.Join(tmpDir, "test.txt")
 | 
					 | 
				
			||||||
	err = os.WriteFile(testFile, []byte(testContent), 0644)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to write test file: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Change to temp directory
 | 
					 | 
				
			||||||
	origDir, _ := os.Getwd()
 | 
					 | 
				
			||||||
	defer os.Chdir(origDir)
 | 
					 | 
				
			||||||
	os.Chdir(tmpDir)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create both isolate and regular commands
 | 
					 | 
				
			||||||
	commands := []utils.ModifyCommand{
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			Name:    "IsolateMultiply",
 | 
					 | 
				
			||||||
			Regex:   `ISOLATE_SECTION.*?value = (?P<value>!num).*?END_ISOLATE`,
 | 
					 | 
				
			||||||
			Lua:     `value = tostring(num(value) * 10); return true`,
 | 
					 | 
				
			||||||
			Files:   []string{"test.txt"},
 | 
					 | 
				
			||||||
			Isolate: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			Name:  "RegularMultiply",
 | 
					 | 
				
			||||||
			Regex: `value = (?P<value>!num)`,
 | 
					 | 
				
			||||||
			Lua:   `value = tostring(num(value) + 100); return true`,
 | 
					 | 
				
			||||||
			Files: []string{"test.txt"},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Associate files with commands
 | 
					 | 
				
			||||||
	files := []string{"test.txt"}
 | 
					 | 
				
			||||||
	associations, err := utils.AssociateFilesWithCommands(files, commands)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to associate files with commands: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify the association
 | 
					 | 
				
			||||||
	association := associations["test.txt"]
 | 
					 | 
				
			||||||
	assert.Len(t, association.IsolateCommands, 1, "Expected 1 isolate command")
 | 
					 | 
				
			||||||
	assert.Len(t, association.Commands, 1, "Expected 1 regular command")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// First run isolate commands
 | 
					 | 
				
			||||||
	isolateResult, err := RunIsolateCommands(association, "test.txt", testContent)
 | 
					 | 
				
			||||||
	if err != nil && err != NothingToDo {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to run isolate commands: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify isolate command result
 | 
					 | 
				
			||||||
	assert.Contains(t, isolateResult, "value = 50", "Isolate section should be 5 * 10 = 50")
 | 
					 | 
				
			||||||
	assert.Contains(t, isolateResult, "value = 10", "Regular section should be unchanged by isolate commands")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Then run regular commands
 | 
					 | 
				
			||||||
	commandLoggers := make(map[string]*logger.Logger)
 | 
					 | 
				
			||||||
	finalResult, err := RunOtherCommands("test.txt", isolateResult, association, commandLoggers)
 | 
					 | 
				
			||||||
	if err != nil && err != NothingToDo {
 | 
					 | 
				
			||||||
		t.Fatalf("Failed to run regular commands: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify final results - regular commands should affect ALL values
 | 
					 | 
				
			||||||
	assert.Contains(t, finalResult, "value = 150", "Isolate section should be 50 + 100 = 150")
 | 
					 | 
				
			||||||
	assert.Contains(t, finalResult, "value = 110", "Regular section should be 10 + 100 = 110")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Logf("Original content:\n%s\n", testContent)
 | 
					 | 
				
			||||||
	t.Logf("After isolate commands:\n%s\n", isolateResult)
 | 
					 | 
				
			||||||
	t.Logf("Final result:\n%s\n", finalResult)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										56
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								main.go
									
									
									
									
									
								
							@@ -58,8 +58,6 @@ func main() {
 | 
				
			|||||||
		fmt.Fprintf(os.Stderr, "      If expression starts with an operator like *, /, +, -, =, etc., v1 is automatically prepended\n")
 | 
							fmt.Fprintf(os.Stderr, "      If expression starts with an operator like *, /, +, -, =, etc., v1 is automatically prepended\n")
 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "      You can use any valid Lua code, including if statements, loops, etc.\n")
 | 
							fmt.Fprintf(os.Stderr, "      You can use any valid Lua code, including if statements, loops, etc.\n")
 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "      Glob patterns are supported for file selection (*.xml, data/**.xml, etc.)\n")
 | 
							fmt.Fprintf(os.Stderr, "      Glob patterns are supported for file selection (*.xml, data/**.xml, etc.)\n")
 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "\nLua Functions Available:\n")
 | 
					 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "%s\n", processor.GetLuaFunctionsHelp())
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// TODO: Fix bed shitting when doing *.yml in barotrauma directory
 | 
						// TODO: Fix bed shitting when doing *.yml in barotrauma directory
 | 
				
			||||||
	flag.Parse()
 | 
						flag.Parse()
 | 
				
			||||||
@@ -82,7 +80,7 @@ func main() {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	mainLogger.Debug("Database connection established")
 | 
						mainLogger.Debug("Database connection established")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	workdone, err := HandleSpecialArgs(args, db)
 | 
						workdone, err := HandleSpecialArgs(args, err, db)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		mainLogger.Error("Failed to handle special args: %v", err)
 | 
							mainLogger.Error("Failed to handle special args: %v", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -366,34 +364,28 @@ func main() {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func HandleSpecialArgs(args []string, db utils.DB) (bool, error) {
 | 
					func HandleSpecialArgs(args []string, err error, db utils.DB) (bool, error) {
 | 
				
			||||||
	handleSpecialArgsLogger := logger.Default.WithPrefix("HandleSpecialArgs")
 | 
						handleSpecialArgsLogger := logger.Default.WithPrefix("HandleSpecialArgs")
 | 
				
			||||||
	handleSpecialArgsLogger.Debug("Handling special arguments: %v", args)
 | 
						handleSpecialArgsLogger.Debug("Handling special arguments: %v", args)
 | 
				
			||||||
	if len(args) == 0 {
 | 
					 | 
				
			||||||
		handleSpecialArgsLogger.Warning("No arguments provided to HandleSpecialArgs")
 | 
					 | 
				
			||||||
		return false, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	switch args[0] {
 | 
						switch args[0] {
 | 
				
			||||||
	case "reset":
 | 
						case "reset":
 | 
				
			||||||
		handleSpecialArgsLogger.Info("Resetting all files to their original state from database")
 | 
							handleSpecialArgsLogger.Info("Resetting all files")
 | 
				
			||||||
		err := utils.ResetAllFiles(db)
 | 
							err = utils.ResetAllFiles(db)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			handleSpecialArgsLogger.Error("Failed to reset all files: %v", err)
 | 
								handleSpecialArgsLogger.Error("Failed to reset all files: %v", err)
 | 
				
			||||||
			return true, err
 | 
								return true, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		handleSpecialArgsLogger.Info("Successfully reset all files to original state")
 | 
							handleSpecialArgsLogger.Info("All files reset")
 | 
				
			||||||
		return true, nil
 | 
							return true, nil
 | 
				
			||||||
	case "dump":
 | 
						case "dump":
 | 
				
			||||||
		handleSpecialArgsLogger.Info("Dumping all files from database (clearing snapshots)")
 | 
							handleSpecialArgsLogger.Info("Dumping all files from database")
 | 
				
			||||||
		err := db.RemoveAllFiles()
 | 
							err = db.RemoveAllFiles()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			handleSpecialArgsLogger.Error("Failed to remove all files from database: %v", err)
 | 
								handleSpecialArgsLogger.Error("Failed to remove all files from database: %v", err)
 | 
				
			||||||
			return true, err
 | 
								return true, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		handleSpecialArgsLogger.Info("Successfully cleared all file snapshots from database")
 | 
							handleSpecialArgsLogger.Info("All files removed from database")
 | 
				
			||||||
		return true, nil
 | 
							return true, nil
 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		handleSpecialArgsLogger.Debug("Unknown special argument: %q", args[0])
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	handleSpecialArgsLogger.Debug("No special arguments handled, returning false")
 | 
						handleSpecialArgsLogger.Debug("No special arguments handled, returning false")
 | 
				
			||||||
	return false, nil
 | 
						return false, nil
 | 
				
			||||||
@@ -642,13 +634,11 @@ func RunIsolateCommands(association utils.FileCommandAssociation, file string, f
 | 
				
			|||||||
	runIsolateCommandsLogger.Trace("File data before isolate modifications: %s", utils.LimitString(fileDataStr, 200))
 | 
						runIsolateCommandsLogger.Trace("File data before isolate modifications: %s", utils.LimitString(fileDataStr, 200))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	anythingDone := false
 | 
						anythingDone := false
 | 
				
			||||||
	currentFileData := fileDataStr
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, isolateCommand := range association.IsolateCommands {
 | 
						for _, isolateCommand := range association.IsolateCommands {
 | 
				
			||||||
		// Check if this isolate command should use JSON mode
 | 
							// Check if this isolate command should use JSON mode
 | 
				
			||||||
		if isolateCommand.JSON || *utils.JSON {
 | 
							if isolateCommand.JSON || *utils.JSON {
 | 
				
			||||||
			runIsolateCommandsLogger.Debug("Begin processing file with JSON isolate command %q", isolateCommand.Name)
 | 
								runIsolateCommandsLogger.Debug("Begin processing file with JSON isolate command %q", isolateCommand.Name)
 | 
				
			||||||
			modifications, err := processor.ProcessJSON(currentFileData, isolateCommand, file)
 | 
								modifications, err := processor.ProcessJSON(fileDataStr, isolateCommand, file)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				runIsolateCommandsLogger.Error("Failed to process file with JSON isolate command %q: %v", isolateCommand.Name, err)
 | 
									runIsolateCommandsLogger.Error("Failed to process file with JSON isolate command %q: %v", isolateCommand.Name, err)
 | 
				
			||||||
				continue
 | 
									continue
 | 
				
			||||||
@@ -663,21 +653,15 @@ func RunIsolateCommands(association utils.FileCommandAssociation, file string, f
 | 
				
			|||||||
			runIsolateCommandsLogger.Debug("Executing %d JSON isolate modifications for file", len(modifications))
 | 
								runIsolateCommandsLogger.Debug("Executing %d JSON isolate modifications for file", len(modifications))
 | 
				
			||||||
			runIsolateCommandsLogger.Trace("JSON isolate modifications: %v", modifications)
 | 
								runIsolateCommandsLogger.Trace("JSON isolate modifications: %v", modifications)
 | 
				
			||||||
			var count int
 | 
								var count int
 | 
				
			||||||
			currentFileData, count = utils.ExecuteModifications(modifications, currentFileData)
 | 
								fileDataStr, count = utils.ExecuteModifications(modifications, fileDataStr)
 | 
				
			||||||
			runIsolateCommandsLogger.Trace("File data after JSON isolate modifications: %s", utils.LimitString(currentFileData, 200))
 | 
								runIsolateCommandsLogger.Trace("File data after JSON isolate modifications: %s", utils.LimitString(fileDataStr, 200))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			atomic.AddInt64(&stats.TotalModifications, int64(count))
 | 
								atomic.AddInt64(&stats.TotalModifications, int64(count))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			cmdCount, ok := stats.ModificationsPerCommand.Load(isolateCommand.Name)
 | 
					 | 
				
			||||||
			if !ok {
 | 
					 | 
				
			||||||
				stats.ModificationsPerCommand.Store(isolateCommand.Name, 0)
 | 
					 | 
				
			||||||
				cmdCount = 0
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			stats.ModificationsPerCommand.Store(isolateCommand.Name, cmdCount.(int)+len(modifications))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			runIsolateCommandsLogger.Info("Executed %d JSON isolate modifications for file", count)
 | 
								runIsolateCommandsLogger.Info("Executed %d JSON isolate modifications for file", count)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			// Regular regex processing for isolate commands
 | 
								// Regular regex processing for isolate commands
 | 
				
			||||||
 | 
								runIsolateCommandsLogger.Debug("Begin processing file with isolate command %q", isolateCommand.Regex)
 | 
				
			||||||
			patterns := isolateCommand.Regexes
 | 
								patterns := isolateCommand.Regexes
 | 
				
			||||||
			if len(patterns) == 0 {
 | 
								if len(patterns) == 0 {
 | 
				
			||||||
				patterns = []string{isolateCommand.Regex}
 | 
									patterns = []string{isolateCommand.Regex}
 | 
				
			||||||
@@ -685,8 +669,7 @@ func RunIsolateCommands(association utils.FileCommandAssociation, file string, f
 | 
				
			|||||||
			for idx, pattern := range patterns {
 | 
								for idx, pattern := range patterns {
 | 
				
			||||||
				tmpCmd := isolateCommand
 | 
									tmpCmd := isolateCommand
 | 
				
			||||||
				tmpCmd.Regex = pattern
 | 
									tmpCmd.Regex = pattern
 | 
				
			||||||
				runIsolateCommandsLogger.Debug("Begin processing file with isolate command %q (pattern %d/%d)", isolateCommand.Name, idx+1, len(patterns))
 | 
									modifications, err := processor.ProcessRegex(fileDataStr, tmpCmd, file)
 | 
				
			||||||
				modifications, err := processor.ProcessRegex(currentFileData, tmpCmd, file)
 | 
					 | 
				
			||||||
				if err != nil {
 | 
									if err != nil {
 | 
				
			||||||
					runIsolateCommandsLogger.Error("Failed to process file with isolate command %q (pattern %d/%d): %v", isolateCommand.Name, idx+1, len(patterns), err)
 | 
										runIsolateCommandsLogger.Error("Failed to process file with isolate command %q (pattern %d/%d): %v", isolateCommand.Name, idx+1, len(patterns), err)
 | 
				
			||||||
					continue
 | 
										continue
 | 
				
			||||||
@@ -701,18 +684,11 @@ func RunIsolateCommands(association utils.FileCommandAssociation, file string, f
 | 
				
			|||||||
				runIsolateCommandsLogger.Debug("Executing %d isolate modifications for file", len(modifications))
 | 
									runIsolateCommandsLogger.Debug("Executing %d isolate modifications for file", len(modifications))
 | 
				
			||||||
				runIsolateCommandsLogger.Trace("Isolate modifications: %v", modifications)
 | 
									runIsolateCommandsLogger.Trace("Isolate modifications: %v", modifications)
 | 
				
			||||||
				var count int
 | 
									var count int
 | 
				
			||||||
				currentFileData, count = utils.ExecuteModifications(modifications, currentFileData)
 | 
									fileDataStr, count = utils.ExecuteModifications(modifications, fileDataStr)
 | 
				
			||||||
				runIsolateCommandsLogger.Trace("File data after isolate modifications: %s", utils.LimitString(currentFileData, 200))
 | 
									runIsolateCommandsLogger.Trace("File data after isolate modifications: %s", utils.LimitString(fileDataStr, 200))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				atomic.AddInt64(&stats.TotalModifications, int64(count))
 | 
									atomic.AddInt64(&stats.TotalModifications, int64(count))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				cmdCount, ok := stats.ModificationsPerCommand.Load(isolateCommand.Name)
 | 
					 | 
				
			||||||
				if !ok {
 | 
					 | 
				
			||||||
					stats.ModificationsPerCommand.Store(isolateCommand.Name, 0)
 | 
					 | 
				
			||||||
					cmdCount = 0
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				stats.ModificationsPerCommand.Store(isolateCommand.Name, cmdCount.(int)+len(modifications))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				runIsolateCommandsLogger.Info("Executed %d isolate modifications for file", count)
 | 
									runIsolateCommandsLogger.Info("Executed %d isolate modifications for file", count)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -721,5 +697,5 @@ func RunIsolateCommands(association utils.FileCommandAssociation, file string, f
 | 
				
			|||||||
		runIsolateCommandsLogger.Debug("No isolate modifications were made for file")
 | 
							runIsolateCommandsLogger.Debug("No isolate modifications were made for file")
 | 
				
			||||||
		return fileDataStr, NothingToDo
 | 
							return fileDataStr, NothingToDo
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return currentFileData, nil
 | 
						return fileDataStr, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,13 +4,10 @@ import (
 | 
				
			|||||||
	"cook/utils"
 | 
						"cook/utils"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"sort"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logger "git.site.quack-lab.dev/dave/cylogger"
 | 
						logger "git.site.quack-lab.dev/dave/cylogger"
 | 
				
			||||||
	"github.com/tidwall/gjson"
 | 
						"github.com/tidwall/sjson"
 | 
				
			||||||
	lua "github.com/yuin/gopher-lua"
 | 
						lua "github.com/yuin/gopher-lua"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -89,8 +86,8 @@ func ProcessJSON(content string, command utils.ModifyCommand, filename string) (
 | 
				
			|||||||
		return commands, fmt.Errorf("failed to convert Lua table back to Go: %v", err)
 | 
							return commands, fmt.Errorf("failed to convert Lua table back to Go: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	processJsonLogger.Debug("About to call applyChanges with original data and modified data")
 | 
						// Use surgical JSON editing instead of full replacement
 | 
				
			||||||
	commands, err = applyChanges(content, jsonData, goData)
 | 
						commands, err = applySurgicalJSONChanges(content, jsonData, goData)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		processJsonLogger.Error("Failed to apply surgical JSON changes: %v", err)
 | 
							processJsonLogger.Error("Failed to apply surgical JSON changes: %v", err)
 | 
				
			||||||
		return commands, fmt.Errorf("failed to apply surgical JSON changes: %v", err)
 | 
							return commands, fmt.Errorf("failed to apply surgical JSON changes: %v", err)
 | 
				
			||||||
@@ -101,296 +98,88 @@ func ProcessJSON(content string, command utils.ModifyCommand, filename string) (
 | 
				
			|||||||
	return commands, nil
 | 
						return commands, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// applyJSONChanges compares original and modified data and applies changes surgically
 | 
					// applySurgicalJSONChanges compares original and modified data and applies changes surgically
 | 
				
			||||||
func applyJSONChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) {
 | 
					func applySurgicalJSONChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) {
 | 
				
			||||||
	var commands []utils.ReplaceCommand
 | 
						var commands []utils.ReplaceCommand
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	appliedCommands, err := applyChanges(content, originalData, modifiedData)
 | 
						// Convert both to JSON for comparison
 | 
				
			||||||
	if err == nil && len(appliedCommands) > 0 {
 | 
						originalJSON, err := json.Marshal(originalData)
 | 
				
			||||||
		return appliedCommands, nil
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return commands, fmt.Errorf("failed to marshal original data: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	return commands, fmt.Errorf("failed to make any changes to the json")
 | 
						modifiedJSON, err := json.Marshal(modifiedData)
 | 
				
			||||||
}
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return commands, fmt.Errorf("failed to marshal modified data: %v", err)
 | 
				
			||||||
// applyChanges attempts to make surgical changes while preserving exact formatting
 | 
						}
 | 
				
			||||||
func applyChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) {
 | 
						
 | 
				
			||||||
	var commands []utils.ReplaceCommand
 | 
						// If no changes, return empty commands
 | 
				
			||||||
 | 
						if string(originalJSON) == string(modifiedJSON) {
 | 
				
			||||||
	// Find all changes between original and modified data
 | 
					 | 
				
			||||||
	changes := findDeepChanges("", originalData, modifiedData)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	jsonLogger.Debug("applyChanges: Found %d changes: %v", len(changes), changes)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(changes) == 0 {
 | 
					 | 
				
			||||||
		return commands, nil
 | 
							return commands, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	// Sort removal operations by index in descending order to avoid index shifting
 | 
						// Try true surgical approach that preserves formatting
 | 
				
			||||||
	var removals []string
 | 
						surgicalCommands, err := applyTrueSurgicalChanges(content, originalData, modifiedData)
 | 
				
			||||||
	var additions []string
 | 
						if err == nil && len(surgicalCommands) > 0 {
 | 
				
			||||||
	var valueChanges []string
 | 
							return surgicalCommands, nil
 | 
				
			||||||
 | 
					 | 
				
			||||||
	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)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	jsonLogger.Debug("applyChanges: %d removals, %d additions, %d value changes", len(removals), len(additions), len(valueChanges))
 | 
						// Fall back to full replacement with proper formatting
 | 
				
			||||||
 | 
						modifiedJSONIndented, err := json.MarshalIndent(modifiedData, "", "  ")
 | 
				
			||||||
	// Apply removals first (from end to beginning to avoid index shifting)
 | 
						if err != nil {
 | 
				
			||||||
	for _, removalPath := range removals {
 | 
							return commands, fmt.Errorf("failed to marshal modified data with indentation: %v", err)
 | 
				
			||||||
		actualPath := strings.TrimSuffix(removalPath, "@remove")
 | 
					 | 
				
			||||||
		elementIndex := extractIndexFromRemovalPath(actualPath)
 | 
					 | 
				
			||||||
		arrayPath := getArrayPathFromElementPath(actualPath)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		jsonLogger.Debug("Processing removal: path=%s, index=%d, arrayPath=%s", actualPath, elementIndex, arrayPath)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Find the exact byte range to remove
 | 
					 | 
				
			||||||
		from, to := findArrayElementRemovalRange(content, arrayPath, elementIndex)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		jsonLogger.Debug("Removing bytes %d-%d", from, to)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		commands = append(commands, utils.ReplaceCommand{
 | 
					 | 
				
			||||||
			From: from,
 | 
					 | 
				
			||||||
			To:   to,
 | 
					 | 
				
			||||||
			With: "",
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		jsonLogger.Debug("Added removal command: From=%d, To=%d, With=\"\"", from, to)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	// Apply additions (new fields)
 | 
						commands = append(commands, utils.ReplaceCommand{
 | 
				
			||||||
	for _, additionPath := range additions {
 | 
							From: 0,
 | 
				
			||||||
		actualPath := strings.TrimSuffix(additionPath, "@add")
 | 
							To:   len(content),
 | 
				
			||||||
		newValue := changes[additionPath]
 | 
							With: string(modifiedJSONIndented),
 | 
				
			||||||
 | 
					 | 
				
			||||||
		jsonLogger.Debug("Processing addition: path=%s, value=%v", actualPath, newValue)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Find the parent object to add the field to
 | 
					 | 
				
			||||||
		parentPath := getParentPath(actualPath)
 | 
					 | 
				
			||||||
		fieldName := getFieldName(actualPath)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		jsonLogger.Debug("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.Debug("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.Debug("Inserting at pos %d", startPos)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Convert the new value to JSON string
 | 
					 | 
				
			||||||
		newValueStr := convertValueToJSONString(newValue)
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		// Insert the new field with pretty-printed formatting
 | 
					 | 
				
			||||||
		// Format: ,"fieldName": { ... }
 | 
					 | 
				
			||||||
		insertText := fmt.Sprintf(`,"%s": %s`, fieldName, newValueStr)
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		commands = append(commands, utils.ReplaceCommand{
 | 
					 | 
				
			||||||
			From: startPos,
 | 
					 | 
				
			||||||
			To:   startPos,
 | 
					 | 
				
			||||||
			With: insertText,
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		jsonLogger.Debug("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.Debug("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.Debug("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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		jsonLogger.Debug("Found value at pos %d-%d: %q", startPos, endPos, result.Raw)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Convert the new value to JSON string
 | 
					 | 
				
			||||||
		newValueStr := convertValueToJSONString(newValue)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		jsonLogger.Debug("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.Debug("Added command: From=%d, To=%d, With=%q", int(startPos), int(endPos), newValueStr)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return commands, nil
 | 
						return commands, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// extractIndexFromRemovalPath extracts the array index from a removal path like "Rows.0.Inputs.1@remove"
 | 
					// applyTrueSurgicalChanges attempts to make surgical changes while preserving exact formatting
 | 
				
			||||||
func extractIndexFromRemovalPath(path string) int {
 | 
					func applyTrueSurgicalChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) {
 | 
				
			||||||
	parts := strings.Split(strings.TrimSuffix(path, "@remove"), ".")
 | 
						var commands []utils.ReplaceCommand
 | 
				
			||||||
	if len(parts) > 0 {
 | 
						
 | 
				
			||||||
		lastPart := parts[len(parts)-1]
 | 
						// Find changes by comparing the data structures
 | 
				
			||||||
		if index, err := strconv.Atoi(lastPart); err == nil {
 | 
						changes := findDeepChanges("", originalData, modifiedData)
 | 
				
			||||||
			return index
 | 
						
 | 
				
			||||||
		}
 | 
						if len(changes) == 0 {
 | 
				
			||||||
 | 
							return commands, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return -1
 | 
						
 | 
				
			||||||
}
 | 
						// Apply changes surgically using sjson.Set() to preserve formatting
 | 
				
			||||||
 | 
						modifiedContent := content
 | 
				
			||||||
// getArrayPathFromElementPath converts "Rows.0.Inputs.1" to "Rows.0.Inputs"
 | 
						for path, newValue := range changes {
 | 
				
			||||||
func getArrayPathFromElementPath(elementPath string) string {
 | 
							var err error
 | 
				
			||||||
	parts := strings.Split(elementPath, ".")
 | 
							modifiedContent, err = sjson.Set(modifiedContent, path, newValue)
 | 
				
			||||||
	if len(parts) > 0 {
 | 
					 | 
				
			||||||
		return strings.Join(parts[:len(parts)-1], ".")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	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"
 | 
					 | 
				
			||||||
	case map[string]interface{}:
 | 
					 | 
				
			||||||
		// Handle maps specially to avoid double-escaping of keys
 | 
					 | 
				
			||||||
		var pairs []string
 | 
					 | 
				
			||||||
		for key, val := range v {
 | 
					 | 
				
			||||||
			// The key might already have escaped quotes from Lua, so we need to be careful
 | 
					 | 
				
			||||||
			// If the key already contains escaped quotes, we need to unescape them first
 | 
					 | 
				
			||||||
			keyStr := key
 | 
					 | 
				
			||||||
			if strings.Contains(key, `\"`) {
 | 
					 | 
				
			||||||
				// Key already has escaped quotes, use it as-is
 | 
					 | 
				
			||||||
				keyStr = `"` + key + `"`
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				// Normal key, escape quotes
 | 
					 | 
				
			||||||
				keyStr = `"` + strings.ReplaceAll(key, `"`, `\"`) + `"`
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			valStr := convertValueToJSONString(val)
 | 
					 | 
				
			||||||
			pairs = append(pairs, keyStr+":"+valStr)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return "{" + strings.Join(pairs, ",") + "}"
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		// For other complex types (arrays), we need to use json.Marshal
 | 
					 | 
				
			||||||
		jsonBytes, err := json.Marshal(v)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return "null" // Fallback to null if marshaling fails
 | 
								return nil, fmt.Errorf("failed to apply surgical change at path %s: %v", path, err)
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return string(jsonBytes)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 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
 | 
					 | 
				
			||||||
	arrayResult := gjson.Get(content, arrayPath)
 | 
					 | 
				
			||||||
	if !arrayResult.Exists() || !arrayResult.IsArray() {
 | 
					 | 
				
			||||||
		return -1, -1
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get all array elements
 | 
					 | 
				
			||||||
	elements := arrayResult.Array()
 | 
					 | 
				
			||||||
	if elementIndex >= len(elements) {
 | 
					 | 
				
			||||||
		return -1, -1
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get the target element
 | 
					 | 
				
			||||||
	elementResult := elements[elementIndex]
 | 
					 | 
				
			||||||
	startPos := int(elementResult.Index)
 | 
					 | 
				
			||||||
	endPos := int(elementResult.Index + len(elementResult.Raw))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Handle comma removal properly
 | 
					 | 
				
			||||||
	if elementIndex == 0 && len(elements) > 1 {
 | 
					 | 
				
			||||||
		// First element but not the only one - remove comma after
 | 
					 | 
				
			||||||
		for i := endPos; i < len(content) && i < endPos+50; i++ {
 | 
					 | 
				
			||||||
			if content[i] == ',' {
 | 
					 | 
				
			||||||
				endPos = i + 1
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else if elementIndex == len(elements)-1 && len(elements) > 1 {
 | 
					 | 
				
			||||||
		// Last element and not the only one - remove comma before
 | 
					 | 
				
			||||||
		prevElementEnd := int(elements[elementIndex-1].Index + len(elements[elementIndex-1].Raw))
 | 
					 | 
				
			||||||
		for i := prevElementEnd; i < startPos && i < len(content); i++ {
 | 
					 | 
				
			||||||
			if content[i] == ',' {
 | 
					 | 
				
			||||||
				startPos = i
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// If it's the only element, don't remove any commas
 | 
						
 | 
				
			||||||
 | 
						// If we successfully made changes, create a replacement command
 | 
				
			||||||
	return startPos, endPos
 | 
						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
 | 
					// findDeepChanges recursively finds all paths that need to be changed
 | 
				
			||||||
func findDeepChanges(basePath string, original, modified interface{}) map[string]interface{} {
 | 
					func findDeepChanges(basePath string, original, modified interface{}) map[string]interface{} {
 | 
				
			||||||
	changes := make(map[string]interface{})
 | 
						changes := make(map[string]interface{})
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	switch orig := original.(type) {
 | 
						switch orig := original.(type) {
 | 
				
			||||||
	case map[string]interface{}:
 | 
						case map[string]interface{}:
 | 
				
			||||||
		if mod, ok := modified.(map[string]interface{}); ok {
 | 
							if mod, ok := modified.(map[string]interface{}); ok {
 | 
				
			||||||
			// Check for new keys added in modified data
 | 
								// Check each key in the modified data
 | 
				
			||||||
			for key, modValue := range mod {
 | 
								for key, modValue := range mod {
 | 
				
			||||||
				var currentPath string
 | 
									var currentPath string
 | 
				
			||||||
				if basePath == "" {
 | 
									if basePath == "" {
 | 
				
			||||||
@@ -398,74 +187,57 @@ func findDeepChanges(basePath string, original, modified interface{}) map[string
 | 
				
			|||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					currentPath = basePath + "." + key
 | 
										currentPath = basePath + "." + key
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
									
 | 
				
			||||||
				if origValue, exists := orig[key]; exists {
 | 
									if origValue, exists := orig[key]; exists {
 | 
				
			||||||
					// Key exists in both, check if value changed
 | 
										// Key exists in both, check if value changed
 | 
				
			||||||
					switch modValue.(type) {
 | 
										if !deepEqual(origValue, modValue) {
 | 
				
			||||||
					case map[string]interface{}, []interface{}:
 | 
											// If it's a nested object/array, recurse
 | 
				
			||||||
						// Recursively check nested structures
 | 
											switch modValue.(type) {
 | 
				
			||||||
						nestedChanges := findDeepChanges(currentPath, origValue, modValue)
 | 
											case map[string]interface{}, []interface{}:
 | 
				
			||||||
						for nestedPath, nestedValue := range nestedChanges {
 | 
												nestedChanges := findDeepChanges(currentPath, origValue, modValue)
 | 
				
			||||||
							changes[nestedPath] = nestedValue
 | 
												for nestedPath, nestedValue := range nestedChanges {
 | 
				
			||||||
						}
 | 
													changes[nestedPath] = nestedValue
 | 
				
			||||||
					default:
 | 
												}
 | 
				
			||||||
						// Primitive value - check if changed
 | 
											default:
 | 
				
			||||||
						if !deepEqual(origValue, modValue) {
 | 
												// Primitive value changed
 | 
				
			||||||
							changes[currentPath] = modValue
 | 
												changes[currentPath] = modValue
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					// New key added - mark for addition
 | 
										// New key added
 | 
				
			||||||
					changes[currentPath+"@add"] = modValue
 | 
										changes[currentPath] = modValue
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	case []interface{}:
 | 
						case []interface{}:
 | 
				
			||||||
		if mod, ok := modified.([]interface{}); ok {
 | 
							if mod, ok := modified.([]interface{}); ok {
 | 
				
			||||||
			// Handle array changes by detecting specific element operations
 | 
								// For arrays, check each index
 | 
				
			||||||
			if len(orig) != len(mod) {
 | 
								for i, modValue := range mod {
 | 
				
			||||||
				// Array length changed - detect if it's element removal
 | 
									var currentPath string
 | 
				
			||||||
				if len(orig) > len(mod) {
 | 
									if basePath == "" {
 | 
				
			||||||
					// Element(s) removed - find which ones by comparing content
 | 
										currentPath = fmt.Sprintf("%d", i)
 | 
				
			||||||
					removedIndices := findRemovedArrayElements(orig, mod)
 | 
					 | 
				
			||||||
					for _, removedIndex := range removedIndices {
 | 
					 | 
				
			||||||
						var currentPath string
 | 
					 | 
				
			||||||
						if basePath == "" {
 | 
					 | 
				
			||||||
							currentPath = fmt.Sprintf("%d@remove", removedIndex)
 | 
					 | 
				
			||||||
						} else {
 | 
					 | 
				
			||||||
							currentPath = fmt.Sprintf("%s.%d@remove", basePath, removedIndex)
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
						changes[currentPath] = nil // Mark for removal
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					// Elements added - more complex, skip for now
 | 
										currentPath = fmt.Sprintf("%s.%d", basePath, i)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} else {
 | 
									
 | 
				
			||||||
				// Same length - check individual elements for value changes
 | 
									if i < len(orig) {
 | 
				
			||||||
				for i, modValue := range mod {
 | 
										// Index exists in both, check if value changed
 | 
				
			||||||
					var currentPath string
 | 
										if !deepEqual(orig[i], modValue) {
 | 
				
			||||||
					if basePath == "" {
 | 
											// If it's a nested object/array, recurse
 | 
				
			||||||
						currentPath = strconv.Itoa(i)
 | 
					 | 
				
			||||||
					} else {
 | 
					 | 
				
			||||||
						currentPath = basePath + "." + strconv.Itoa(i)
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					if i < len(orig) {
 | 
					 | 
				
			||||||
						// Index exists in both, check if value changed
 | 
					 | 
				
			||||||
						switch modValue.(type) {
 | 
											switch modValue.(type) {
 | 
				
			||||||
						case map[string]interface{}, []interface{}:
 | 
											case map[string]interface{}, []interface{}:
 | 
				
			||||||
							// Recursively check nested structures
 | 
					 | 
				
			||||||
							nestedChanges := findDeepChanges(currentPath, orig[i], modValue)
 | 
												nestedChanges := findDeepChanges(currentPath, orig[i], modValue)
 | 
				
			||||||
							for nestedPath, nestedValue := range nestedChanges {
 | 
												for nestedPath, nestedValue := range nestedChanges {
 | 
				
			||||||
								changes[nestedPath] = nestedValue
 | 
													changes[nestedPath] = nestedValue
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
						default:
 | 
											default:
 | 
				
			||||||
							// Primitive value - check if changed
 | 
												// Primitive value changed
 | 
				
			||||||
							if !deepEqual(orig[i], modValue) {
 | 
												changes[currentPath] = modValue
 | 
				
			||||||
								changes[currentPath] = modValue
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										// New array element added
 | 
				
			||||||
 | 
										changes[currentPath] = modValue
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -479,31 +251,10 @@ func findDeepChanges(basePath string, original, modified interface{}) map[string
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	return changes
 | 
						return changes
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// findRemovedArrayElements compares two arrays and returns indices of removed elements
 | 
					 | 
				
			||||||
func findRemovedArrayElements(original, modified []interface{}) []int {
 | 
					 | 
				
			||||||
	var removedIndices []int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Simple approach: find elements in original that don't exist in modified
 | 
					 | 
				
			||||||
	for i, origElement := range original {
 | 
					 | 
				
			||||||
		found := false
 | 
					 | 
				
			||||||
		for _, modElement := range modified {
 | 
					 | 
				
			||||||
			if deepEqual(origElement, modElement) {
 | 
					 | 
				
			||||||
				found = true
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if !found {
 | 
					 | 
				
			||||||
			removedIndices = append(removedIndices, i)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return removedIndices
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// deepEqual performs deep comparison of two values
 | 
					// deepEqual performs deep comparison of two values
 | 
				
			||||||
func deepEqual(a, b interface{}) bool {
 | 
					func deepEqual(a, b interface{}) bool {
 | 
				
			||||||
	if a == nil && b == nil {
 | 
						if a == nil && b == nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,25 +16,46 @@ func TestProcessJSON(t *testing.T) {
 | 
				
			|||||||
		expectedMods   int
 | 
							expectedMods   int
 | 
				
			||||||
	}{
 | 
						}{
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name:           "Basic JSON object modification",
 | 
								name:          "Basic JSON object modification",
 | 
				
			||||||
			input:          `{"name": "test", "value": 42}`,
 | 
								input:         `{"name": "test", "value": 42}`,
 | 
				
			||||||
			luaExpression:  `data.value = data.value * 2; return true`,
 | 
								luaExpression: `data.value = data.value * 2; return true`,
 | 
				
			||||||
			expectedOutput: `{"name": "test", "value": 84}`,
 | 
								expectedOutput: `{
 | 
				
			||||||
			expectedMods:   1,
 | 
					  "name": "test",
 | 
				
			||||||
 | 
					  "value": 84
 | 
				
			||||||
 | 
					}`,
 | 
				
			||||||
 | 
								expectedMods: 1,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name:           "JSON array modification",
 | 
								name:          "JSON array modification",
 | 
				
			||||||
			input:          `{"items": [{"name": "item1", "value": 10}, {"name": "item2", "value": 20}]}`,
 | 
								input:         `{"items": [{"id": 1, "value": 10}, {"id": 2, "value": 20}]}`,
 | 
				
			||||||
			luaExpression:  `for i, item in ipairs(data.items) do item.value = item.value * 2 end modified = true`,
 | 
								luaExpression: `for i, item in ipairs(data.items) do data.items[i].value = item.value * 1.5 end; return true`,
 | 
				
			||||||
			expectedOutput: `{"items": [{"name": "item1", "value": 20}, {"name": "item2", "value": 40}]}`,
 | 
								expectedOutput: `{
 | 
				
			||||||
			expectedMods:   2, 
 | 
					  "items": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "id": 1,
 | 
				
			||||||
 | 
					      "value": 15
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "id": 2,
 | 
				
			||||||
 | 
					      "value": 30
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}`,
 | 
				
			||||||
 | 
								expectedMods: 1,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name:           "JSON nested object modification",
 | 
								name:          "JSON nested object modification",
 | 
				
			||||||
			input:          `{"config": {"setting1": {"enabled": true, "value": 5}, "setting2": {"enabled": false, "value": 10}}}`,
 | 
								input:         `{"config": {"settings": {"enabled": false, "timeout": 30}}}`,
 | 
				
			||||||
			luaExpression:  `data.config.setting1.enabled = false data.config.setting2.value = 15 modified = true`,
 | 
								luaExpression: `data.config.settings.enabled = true; data.config.settings.timeout = 60; return true`,
 | 
				
			||||||
			expectedOutput: `{"config": {"setting1": {"enabled": false, "value": 5}, "setting2": {"enabled": false, "value": 15}}}`,
 | 
								expectedOutput: `{
 | 
				
			||||||
			expectedMods:   2,
 | 
					  "config": {
 | 
				
			||||||
 | 
					    "settings": {
 | 
				
			||||||
 | 
					      "enabled": true,
 | 
				
			||||||
 | 
					      "timeout": 60
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}`,
 | 
				
			||||||
 | 
								expectedMods: 1,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name:           "JSON no modification",
 | 
								name:           "JSON no modification",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -247,7 +247,6 @@ modified = false
 | 
				
			|||||||
	initLuaHelpersLogger.Debug("Setting up Lua print function to Go")
 | 
						initLuaHelpersLogger.Debug("Setting up Lua print function to Go")
 | 
				
			||||||
	L.SetGlobal("print", L.NewFunction(printToGo))
 | 
						L.SetGlobal("print", L.NewFunction(printToGo))
 | 
				
			||||||
	L.SetGlobal("fetch", L.NewFunction(fetch))
 | 
						L.SetGlobal("fetch", L.NewFunction(fetch))
 | 
				
			||||||
	L.SetGlobal("re", L.NewFunction(EvalRegex))
 | 
					 | 
				
			||||||
	initLuaHelpersLogger.Debug("Lua print and fetch functions bound to Go")
 | 
						initLuaHelpersLogger.Debug("Lua print and fetch functions bound to Go")
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -482,82 +481,3 @@ func fetch(L *lua.LState) int {
 | 
				
			|||||||
	fetchLogger.Debug("Pushed response table to Lua stack")
 | 
						fetchLogger.Debug("Pushed response table to Lua stack")
 | 
				
			||||||
	return 1
 | 
						return 1
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func EvalRegex(L *lua.LState) int {
 | 
					 | 
				
			||||||
	evalRegexLogger := processorLogger.WithPrefix("evalRegex")
 | 
					 | 
				
			||||||
	evalRegexLogger.Debug("Lua evalRegex function called")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	pattern := L.ToString(1)
 | 
					 | 
				
			||||||
	input := L.ToString(2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	evalRegexLogger.Debug("Pattern: %q, Input: %q", pattern, input)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	re := regexp.MustCompile(pattern)
 | 
					 | 
				
			||||||
	matches := re.FindStringSubmatch(input)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	evalRegexLogger.Debug("Go regex matches: %v (count: %d)", matches, len(matches))
 | 
					 | 
				
			||||||
	evalRegexLogger.Debug("Matches is nil: %t", matches == nil)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(matches) > 0 {
 | 
					 | 
				
			||||||
		matchesTable := L.NewTable()
 | 
					 | 
				
			||||||
		for i, match := range matches {
 | 
					 | 
				
			||||||
			matchesTable.RawSetString(fmt.Sprintf("%d", i), lua.LString(match))
 | 
					 | 
				
			||||||
			evalRegexLogger.Debug("Set table[%d] = %q", i, match)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		L.Push(matchesTable)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		L.Push(lua.LNil)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	evalRegexLogger.Debug("Pushed matches table to Lua stack")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return 1
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// GetLuaFunctionsHelp returns a comprehensive help string for all available Lua functions
 | 
					 | 
				
			||||||
func GetLuaFunctionsHelp() string {
 | 
					 | 
				
			||||||
	return `Lua Functions Available in Global Environment:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
MATH FUNCTIONS:
 | 
					 | 
				
			||||||
  min(a, b)           - Returns the minimum of two numbers
 | 
					 | 
				
			||||||
  max(a, b)           - Returns the maximum of two numbers
 | 
					 | 
				
			||||||
  round(x, n)         - Rounds x to n decimal places (default 0)
 | 
					 | 
				
			||||||
  floor(x)            - Returns the floor of x
 | 
					 | 
				
			||||||
  ceil(x)             - Returns the ceiling of x
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
STRING FUNCTIONS:
 | 
					 | 
				
			||||||
  upper(s)            - Converts string to uppercase
 | 
					 | 
				
			||||||
  lower(s)            - Converts string to lowercase
 | 
					 | 
				
			||||||
  format(s, ...)      - Formats string using Lua string.format
 | 
					 | 
				
			||||||
  trim(s)             - Removes leading/trailing whitespace
 | 
					 | 
				
			||||||
  strsplit(inputstr, sep) - Splits string by separator (default: whitespace)
 | 
					 | 
				
			||||||
  num(str)            - Converts string to number (returns 0 if invalid)
 | 
					 | 
				
			||||||
  str(num)            - Converts number to string
 | 
					 | 
				
			||||||
  is_number(str)      - Returns true if string is numeric
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
TABLE FUNCTIONS:
 | 
					 | 
				
			||||||
  DumpTable(table, depth) - Prints table structure recursively
 | 
					 | 
				
			||||||
  isArray(t)          - Returns true if table is a sequential array
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
HTTP FUNCTIONS:
 | 
					 | 
				
			||||||
  fetch(url, options) - Makes HTTP request, returns response table
 | 
					 | 
				
			||||||
    options: {method="GET", headers={}, body=""}
 | 
					 | 
				
			||||||
    returns: {status, statusText, ok, body, headers}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
REGEX FUNCTIONS:
 | 
					 | 
				
			||||||
  re(pattern, input)  - Applies regex pattern to input string
 | 
					 | 
				
			||||||
    returns: table with matches (index 0 = full match, 1+ = groups)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
UTILITY FUNCTIONS:
 | 
					 | 
				
			||||||
  print(...)          - Prints arguments to Go logger
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
EXAMPLES:
 | 
					 | 
				
			||||||
  round(3.14159, 2)   -> 3.14
 | 
					 | 
				
			||||||
  strsplit("a,b,c", ",") -> {"a", "b", "c"}
 | 
					 | 
				
			||||||
  upper("hello")      -> "HELLO"
 | 
					 | 
				
			||||||
  min(5, 3)           -> 3
 | 
					 | 
				
			||||||
  num("123")          -> 123
 | 
					 | 
				
			||||||
  is_number("abc")    -> false
 | 
					 | 
				
			||||||
  fetch("https://api.example.com/data")
 | 
					 | 
				
			||||||
  re("(\\w+)@(\\w+)", "user@domain.com") -> {"user@domain.com", "user", "domain.com"}`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,148 +0,0 @@
 | 
				
			|||||||
package processor_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
					 | 
				
			||||||
	lua "github.com/yuin/gopher-lua"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"cook/processor"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Happy Path: Function correctly returns all regex capture groups as Lua table when given valid pattern and input.
 | 
					 | 
				
			||||||
func TestEvalRegex_CaptureGroupsReturned(t *testing.T) {
 | 
					 | 
				
			||||||
	L := lua.NewState()
 | 
					 | 
				
			||||||
	defer L.Close()
 | 
					 | 
				
			||||||
	pattern := `(\w+)-(\d+)`
 | 
					 | 
				
			||||||
	input := "test-42"
 | 
					 | 
				
			||||||
	L.Push(lua.LString(pattern))
 | 
					 | 
				
			||||||
	L.Push(lua.LString(input))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	result := processor.EvalRegex(L)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.Equal(t, 1, result, "Expected return value to be 1 (one value pushed to Lua stack)")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	out := L.Get(-1)
 | 
					 | 
				
			||||||
	tbl, ok := out.(*lua.LTable)
 | 
					 | 
				
			||||||
	if !ok {
 | 
					 | 
				
			||||||
		t.Fatalf("Expected Lua table, got %T", out)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	expected := []string{"test-42", "test", "42"}
 | 
					 | 
				
			||||||
	for i, v := range expected {
 | 
					 | 
				
			||||||
		val := tbl.RawGetString(fmt.Sprintf("%d", i))
 | 
					 | 
				
			||||||
		assert.Equal(t, lua.LString(v), val, "Expected index %d to be %q", i, v)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Happy Path: Function returns nil when regex pattern does not match input string.
 | 
					 | 
				
			||||||
func TestEvalRegex_NoMatchReturnsNil(t *testing.T) {
 | 
					 | 
				
			||||||
	L := lua.NewState()
 | 
					 | 
				
			||||||
	defer L.Close()
 | 
					 | 
				
			||||||
	L.Push(lua.LString(`(foo)(bar)`))
 | 
					 | 
				
			||||||
	L.Push(lua.LString("no-match-here"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	result := processor.EvalRegex(L)
 | 
					 | 
				
			||||||
	assert.Equal(t, 1, result, "Expected return value to be 1 (one value pushed to Lua stack)")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	out := L.Get(-1)
 | 
					 | 
				
			||||||
	// Should be nil when no matches found
 | 
					 | 
				
			||||||
	assert.Equal(t, lua.LNil, out, "Expected nil when no matches found")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Happy Path: Function handles patterns with no capture groups by returning the full match in the Lua table.
 | 
					 | 
				
			||||||
func TestEvalRegex_NoCaptureGroups(t *testing.T) {
 | 
					 | 
				
			||||||
	L := lua.NewState()
 | 
					 | 
				
			||||||
	defer L.Close()
 | 
					 | 
				
			||||||
	pattern := `foo\d+`
 | 
					 | 
				
			||||||
	input := "foo123"
 | 
					 | 
				
			||||||
	L.Push(lua.LString(pattern))
 | 
					 | 
				
			||||||
	L.Push(lua.LString(input))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	result := processor.EvalRegex(L)
 | 
					 | 
				
			||||||
	assert.Equal(t, 1, result, "Expected return value to be 1 (one value pushed to Lua stack)")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	out := L.Get(-1)
 | 
					 | 
				
			||||||
	tbl, ok := out.(*lua.LTable)
 | 
					 | 
				
			||||||
	if !ok {
 | 
					 | 
				
			||||||
		t.Fatalf("Expected Lua table, got %T", out)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	fullMatch := tbl.RawGetString("0")
 | 
					 | 
				
			||||||
	assert.Equal(t, lua.LString("foo123"), fullMatch)
 | 
					 | 
				
			||||||
	// There should be only the full match (index 0)
 | 
					 | 
				
			||||||
	count := 0
 | 
					 | 
				
			||||||
	tbl.ForEach(func(k, v lua.LValue) {
 | 
					 | 
				
			||||||
		count++
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	assert.Equal(t, 1, count)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Edge Case: Function handles invalid regex pattern by letting regexp.MustCompile panic (which is expected behavior)
 | 
					 | 
				
			||||||
func TestEvalRegex_InvalidPattern(t *testing.T) {
 | 
					 | 
				
			||||||
	L := lua.NewState()
 | 
					 | 
				
			||||||
	defer L.Close()
 | 
					 | 
				
			||||||
	pattern := `([a-z` // invalid regex
 | 
					 | 
				
			||||||
	L.Push(lua.LString(pattern))
 | 
					 | 
				
			||||||
	L.Push(lua.LString("someinput"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// This should panic due to invalid regex pattern
 | 
					 | 
				
			||||||
	assert.Panics(t, func() {
 | 
					 | 
				
			||||||
		processor.EvalRegex(L)
 | 
					 | 
				
			||||||
	}, "Expected panic for invalid regex pattern")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Edge Case: Function returns nil when input string is empty and pattern doesn't match.
 | 
					 | 
				
			||||||
func TestEvalRegex_EmptyInputString(t *testing.T) {
 | 
					 | 
				
			||||||
	L := lua.NewState()
 | 
					 | 
				
			||||||
	defer L.Close()
 | 
					 | 
				
			||||||
	L.Push(lua.LString(`(foo)`))
 | 
					 | 
				
			||||||
	L.Push(lua.LString(""))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	result := processor.EvalRegex(L)
 | 
					 | 
				
			||||||
	assert.Equal(t, 1, result, "Expected return value to be 1 (one value pushed to Lua stack)")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	out := L.Get(-1)
 | 
					 | 
				
			||||||
	// Should be nil when no matches found
 | 
					 | 
				
			||||||
	assert.Equal(t, lua.LNil, out, "Expected nil when input is empty and pattern doesn't match")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Edge Case: Function handles nil or missing arguments gracefully without causing a runtime panic.
 | 
					 | 
				
			||||||
func TestEvalRegex_MissingArguments(t *testing.T) {
 | 
					 | 
				
			||||||
	L := lua.NewState()
 | 
					 | 
				
			||||||
	defer L.Close()
 | 
					 | 
				
			||||||
	defer func() {
 | 
					 | 
				
			||||||
		if r := recover(); r != nil {
 | 
					 | 
				
			||||||
			t.Errorf("Did not expect panic when arguments are missing, got: %v", r)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
	// No arguments pushed at all
 | 
					 | 
				
			||||||
	processor.EvalRegex(L)
 | 
					 | 
				
			||||||
	// Should just not match anything or produce empty table, but must not panic
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestEvalComplexRegex(t *testing.T) {
 | 
					 | 
				
			||||||
	// Test complex regex pattern with multiple capture groups
 | 
					 | 
				
			||||||
	L := lua.NewState()
 | 
					 | 
				
			||||||
	defer L.Close()
 | 
					 | 
				
			||||||
	pattern := `^((Bulk_)?(Pistol|Rifle).*?Round.*?)$`
 | 
					 | 
				
			||||||
	input := "Pistol_Round"
 | 
					 | 
				
			||||||
	L.Push(lua.LString(pattern))
 | 
					 | 
				
			||||||
	L.Push(lua.LString(input))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	processor.EvalRegex(L)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	out := L.Get(-1)
 | 
					 | 
				
			||||||
	tbl, ok := out.(*lua.LTable)
 | 
					 | 
				
			||||||
	if !ok {
 | 
					 | 
				
			||||||
		t.Fatalf("Expected Lua table, got %T", out)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Pattern should match: ["Pistol_Round", "Pistol_Round", "", "Pistol"]
 | 
					 | 
				
			||||||
	// This creates 4 elements in the matches array, not 1
 | 
					 | 
				
			||||||
	expectedCount := 4
 | 
					 | 
				
			||||||
	actualCount := 0
 | 
					 | 
				
			||||||
	tbl.ForEach(func(k, v lua.LValue) {
 | 
					 | 
				
			||||||
		actualCount++
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	assert.Equal(t, expectedCount, actualCount, "Expected %d matches for pattern %q with input %q", expectedCount, pattern, input)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -3,8 +3,6 @@ package processor
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"cook/utils"
 | 
						"cook/utils"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/google/go-cmp/cmp"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestSurgicalJSONEditing(t *testing.T) {
 | 
					func TestSurgicalJSONEditing(t *testing.T) {
 | 
				
			||||||
@@ -43,8 +41,9 @@ modified = true
 | 
				
			|||||||
`,
 | 
					`,
 | 
				
			||||||
			expected: `{
 | 
								expected: `{
 | 
				
			||||||
  "name": "test",
 | 
					  "name": "test",
 | 
				
			||||||
  "value": 42
 | 
					  "value": 42,
 | 
				
			||||||
,"newField": "added"}`, // sjson.Set() adds new fields in compact format
 | 
					  "newField": "added"
 | 
				
			||||||
 | 
					}`,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name: "Modify nested field",
 | 
								name: "Modify nested field",
 | 
				
			||||||
@@ -94,14 +93,19 @@ modified = true
 | 
				
			|||||||
				result = result[:cmd.From] + cmd.With + result[cmd.To:]
 | 
									result = result[:cmd.From] + cmd.With + result[cmd.To:]
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			diff := cmp.Diff(result, tt.expected)
 | 
								// Instead of exact string comparison, check that key values are present
 | 
				
			||||||
			if diff != "" {
 | 
								// This accounts for field ordering differences in JSON
 | 
				
			||||||
				t.Errorf("Differences:\n%s", diff)
 | 
								if !contains(result, `"value": 84`) && tt.name == "Modify single field" {
 | 
				
			||||||
 | 
									t.Errorf("Expected value to be 84, got:\n%s", result)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								if !contains(result, `"newField": "added"`) && tt.name == "Add new field" {
 | 
				
			||||||
			// Check the actual result matches expected
 | 
									t.Errorf("Expected newField to be added, got:\n%s", result)
 | 
				
			||||||
			if result != tt.expected {
 | 
								}
 | 
				
			||||||
				t.Errorf("Expected:\n%s\n\nGot:\n%s", tt.expected, result)
 | 
								if !contains(result, `"enabled": true`) && tt.name == "Modify nested field" {
 | 
				
			||||||
 | 
									t.Errorf("Expected enabled to be true, got:\n%s", result)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if !contains(result, `"timeout": 60`) && tt.name == "Modify nested field" {
 | 
				
			||||||
 | 
									t.Errorf("Expected timeout to be 60, got:\n%s", result)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -135,32 +139,6 @@ func TestSurgicalJSONPreservesFormatting(t *testing.T) {
 | 
				
			|||||||
  ]
 | 
					  ]
 | 
				
			||||||
}`
 | 
					}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	expected := `{
 | 
					 | 
				
			||||||
  "Defaults": {
 | 
					 | 
				
			||||||
    "Behaviour": "None",
 | 
					 | 
				
			||||||
    "Description": "",
 | 
					 | 
				
			||||||
    "DisplayName": "",
 | 
					 | 
				
			||||||
    "FlavorText": "",
 | 
					 | 
				
			||||||
    "Icon": "None",
 | 
					 | 
				
			||||||
    "MaxStack": 1,
 | 
					 | 
				
			||||||
    "Override_Glow_Icon": "None",
 | 
					 | 
				
			||||||
    "Weight": 0,
 | 
					 | 
				
			||||||
    "bAllowZeroWeight": false
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "RowStruct": "/Script/Icarus.ItemableData",
 | 
					 | 
				
			||||||
  "Rows": [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "Description": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-Description\", \"A bundle of soft fiber, highly useful.\")",
 | 
					 | 
				
			||||||
      "DisplayName": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-DisplayName\", \"Fiber\")",
 | 
					 | 
				
			||||||
      "FlavorText": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-FlavorText\", \"Fiber is collected from bast, the strong inner bark of certain flowering plants.\")",
 | 
					 | 
				
			||||||
      "Icon": "/Game/Assets/2DArt/UI/Items/Item_Icons/Resources/ITEM_Fibre.ITEM_Fibre",
 | 
					 | 
				
			||||||
      "MaxStack": 1000000,
 | 
					 | 
				
			||||||
      "Name": "Item_Fiber",
 | 
					 | 
				
			||||||
      "Weight": 500
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  ]
 | 
					 | 
				
			||||||
}`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	command := utils.ModifyCommand{
 | 
						command := utils.ModifyCommand{
 | 
				
			||||||
		Name: "test",
 | 
							Name: "test",
 | 
				
			||||||
		Lua: `
 | 
							Lua: `
 | 
				
			||||||
@@ -185,341 +163,14 @@ modified = true
 | 
				
			|||||||
		result = result[:cmd.From] + cmd.With + result[cmd.To:]
 | 
							result = result[:cmd.From] + cmd.With + result[cmd.To:]
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	diff := cmp.Diff(result, expected)
 | 
						// Check that the weight was changed
 | 
				
			||||||
	if diff != "" {
 | 
						if !contains(result, `"Weight": 500`) {
 | 
				
			||||||
		t.Errorf("Differences:\n%s", diff)
 | 
							t.Errorf("Expected weight to be changed to 500, got:\n%s", result)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check that the result matches expected (preserves formatting and changes weight)
 | 
						// Check that formatting is preserved (should have proper indentation)
 | 
				
			||||||
	if result != expected {
 | 
						if !contains(result, "  \"Weight\": 500") {
 | 
				
			||||||
		t.Errorf("Expected:\n%s\n\nGot:\n%s", expected, result)
 | 
							t.Errorf("Expected proper indentation, got:\n%s", result)
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestSurgicalJSONPreservesFormatting2(t *testing.T) {
 | 
					 | 
				
			||||||
	// Test that surgical editing preserves the original formatting structure
 | 
					 | 
				
			||||||
	content := `
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "RowStruct": "/Script/Icarus.ProcessorRecipe",
 | 
					 | 
				
			||||||
  "Defaults": {
 | 
					 | 
				
			||||||
    "bForceDisableRecipe": false,
 | 
					 | 
				
			||||||
    "Requirement": {
 | 
					 | 
				
			||||||
      "RowName": "None",
 | 
					 | 
				
			||||||
      "DataTableName": "D_Talents"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "SessionRequirement": {
 | 
					 | 
				
			||||||
      "RowName": "None",
 | 
					 | 
				
			||||||
      "DataTableName": "D_CharacterFlags"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "CharacterRequirement": {
 | 
					 | 
				
			||||||
      "RowName": "None",
 | 
					 | 
				
			||||||
      "DataTableName": "D_CharacterFlags"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "RequiredMillijoules": 2500,
 | 
					 | 
				
			||||||
    "RecipeSets": [],
 | 
					 | 
				
			||||||
    "ResourceCostMultipliers": [],
 | 
					 | 
				
			||||||
    "Inputs": [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "Element": {
 | 
					 | 
				
			||||||
          "RowName": "None",
 | 
					 | 
				
			||||||
          "DataTableName": "D_ItemsStatic"
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "Count": 1,
 | 
					 | 
				
			||||||
        "DynamicProperties": []
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    "Container": {
 | 
					 | 
				
			||||||
      "Value": "None"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "ResourceInputs": [],
 | 
					 | 
				
			||||||
    "bSelectOutputItemRandomly": false,
 | 
					 | 
				
			||||||
    "bContainsContainer": false,
 | 
					 | 
				
			||||||
    "ItemIconOverride": {
 | 
					 | 
				
			||||||
      "ItemStaticData": {
 | 
					 | 
				
			||||||
        "RowName": "None",
 | 
					 | 
				
			||||||
        "DataTableName": "D_ItemsStatic"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "ItemDynamicData": [],
 | 
					 | 
				
			||||||
      "ItemCustomStats": [],
 | 
					 | 
				
			||||||
      "CustomProperties": {
 | 
					 | 
				
			||||||
        "StaticWorldStats": [],
 | 
					 | 
				
			||||||
        "StaticWorldHeldStats": [],
 | 
					 | 
				
			||||||
        "Stats": [],
 | 
					 | 
				
			||||||
        "Alterations": [],
 | 
					 | 
				
			||||||
        "LivingItemSlots": []
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "DatabaseGUID": "",
 | 
					 | 
				
			||||||
      "ItemOwnerLookupId": -1,
 | 
					 | 
				
			||||||
      "RuntimeTags": {
 | 
					 | 
				
			||||||
        "GameplayTags": []
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "Outputs": [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "Element": {
 | 
					 | 
				
			||||||
          "RowName": "None",
 | 
					 | 
				
			||||||
          "DataTableName": "D_ItemTemplate"
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "Count": 1,
 | 
					 | 
				
			||||||
        "DynamicProperties": []
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    "ResourceOutputs": [],
 | 
					 | 
				
			||||||
    "Refundable": "Inherit",
 | 
					 | 
				
			||||||
    "ExperienceMultiplier": 1,
 | 
					 | 
				
			||||||
    "Audio": {
 | 
					 | 
				
			||||||
      "RowName": "None",
 | 
					 | 
				
			||||||
      "DataTableName": "D_CraftingAudioData"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "Rows": [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "Name": "Biofuel1",
 | 
					 | 
				
			||||||
      "RecipeSets": [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "RowName": "Composter",
 | 
					 | 
				
			||||||
          "DataTableName": "D_RecipeSets"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "Inputs": [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "Element": {
 | 
					 | 
				
			||||||
            "RowName": "Raw_Meat",
 | 
					 | 
				
			||||||
            "DataTableName": "D_ItemsStatic"
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "Count": 2,
 | 
					 | 
				
			||||||
          "DynamicProperties": []
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "Element": {
 | 
					 | 
				
			||||||
            "RowName": "Tree_Sap",
 | 
					 | 
				
			||||||
            "DataTableName": "D_ItemsStatic"
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "Count": 1,
 | 
					 | 
				
			||||||
          "DynamicProperties": []
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "Outputs": [],
 | 
					 | 
				
			||||||
      "Audio": {
 | 
					 | 
				
			||||||
        "RowName": "Composter"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "ResourceOutputs": [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "Type": {
 | 
					 | 
				
			||||||
            "Value": "Biofuel"
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "RequiredUnits": 100
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
  `
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	expected := `
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "RowStruct": "/Script/Icarus.ProcessorRecipe",
 | 
					 | 
				
			||||||
  "Defaults": {
 | 
					 | 
				
			||||||
    "bForceDisableRecipe": false,
 | 
					 | 
				
			||||||
    "Requirement": {
 | 
					 | 
				
			||||||
      "RowName": "None",
 | 
					 | 
				
			||||||
      "DataTableName": "D_Talents"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "SessionRequirement": {
 | 
					 | 
				
			||||||
      "RowName": "None",
 | 
					 | 
				
			||||||
      "DataTableName": "D_CharacterFlags"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "CharacterRequirement": {
 | 
					 | 
				
			||||||
      "RowName": "None",
 | 
					 | 
				
			||||||
      "DataTableName": "D_CharacterFlags"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "RequiredMillijoules": 2500,
 | 
					 | 
				
			||||||
    "RecipeSets": [],
 | 
					 | 
				
			||||||
    "ResourceCostMultipliers": [],
 | 
					 | 
				
			||||||
    "Inputs": [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "Element": {
 | 
					 | 
				
			||||||
          "RowName": "None",
 | 
					 | 
				
			||||||
          "DataTableName": "D_ItemsStatic"
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "Count": 1,
 | 
					 | 
				
			||||||
        "DynamicProperties": []
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    "Container": {
 | 
					 | 
				
			||||||
      "Value": "None"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "ResourceInputs": [],
 | 
					 | 
				
			||||||
    "bSelectOutputItemRandomly": false,
 | 
					 | 
				
			||||||
    "bContainsContainer": false,
 | 
					 | 
				
			||||||
    "ItemIconOverride": {
 | 
					 | 
				
			||||||
      "ItemStaticData": {
 | 
					 | 
				
			||||||
        "RowName": "None",
 | 
					 | 
				
			||||||
        "DataTableName": "D_ItemsStatic"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "ItemDynamicData": [],
 | 
					 | 
				
			||||||
      "ItemCustomStats": [],
 | 
					 | 
				
			||||||
      "CustomProperties": {
 | 
					 | 
				
			||||||
        "StaticWorldStats": [],
 | 
					 | 
				
			||||||
        "StaticWorldHeldStats": [],
 | 
					 | 
				
			||||||
        "Stats": [],
 | 
					 | 
				
			||||||
        "Alterations": [],
 | 
					 | 
				
			||||||
        "LivingItemSlots": []
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "DatabaseGUID": "",
 | 
					 | 
				
			||||||
      "ItemOwnerLookupId": -1,
 | 
					 | 
				
			||||||
      "RuntimeTags": {
 | 
					 | 
				
			||||||
        "GameplayTags": []
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "Outputs": [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "Element": {
 | 
					 | 
				
			||||||
          "RowName": "None",
 | 
					 | 
				
			||||||
          "DataTableName": "D_ItemTemplate"
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "Count": 1,
 | 
					 | 
				
			||||||
        "DynamicProperties": []
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    "ResourceOutputs": [],
 | 
					 | 
				
			||||||
    "Refundable": "Inherit",
 | 
					 | 
				
			||||||
    "ExperienceMultiplier": 1,
 | 
					 | 
				
			||||||
    "Audio": {
 | 
					 | 
				
			||||||
      "RowName": "None",
 | 
					 | 
				
			||||||
      "DataTableName": "D_CraftingAudioData"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "Rows": [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "Name": "Biofuel1",
 | 
					 | 
				
			||||||
      "RecipeSets": [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "RowName": "Composter",
 | 
					 | 
				
			||||||
          "DataTableName": "D_RecipeSets"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "Inputs": [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "Element": {
 | 
					 | 
				
			||||||
            "RowName": "Raw_Meat",
 | 
					 | 
				
			||||||
            "DataTableName": "D_ItemsStatic"
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "Count": 2,
 | 
					 | 
				
			||||||
          "DynamicProperties": []
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "Outputs": [],
 | 
					 | 
				
			||||||
      "Audio": {
 | 
					 | 
				
			||||||
        "RowName": "Composter"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "ResourceOutputs": [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "Type": {
 | 
					 | 
				
			||||||
            "Value": "Biofuel"
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "RequiredUnits": 100
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
  `
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	command := utils.ModifyCommand{
 | 
					 | 
				
			||||||
		Name: "test",
 | 
					 | 
				
			||||||
		Lua: `
 | 
					 | 
				
			||||||
    -- Define regex patterns for matching recipe names
 | 
					 | 
				
			||||||
    local function matchesPattern(name, pattern)
 | 
					 | 
				
			||||||
      local matches = re(pattern, name)
 | 
					 | 
				
			||||||
      -- Check if matches table has any content (index 0 or 1 should exist if there's a match)
 | 
					 | 
				
			||||||
      return matches and (matches[0] or matches[1])
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    -- Selection pattern for recipes that get multiplied
 | 
					 | 
				
			||||||
    local selectionPattern = "(?-s)(Bulk_)?(Pistol|Rifle).*?Round.*?|(Carbon|Composite)_Paste.*|(Gold|Copper)_Wire|(Ironw|Copper)_Nail|(Platinum|Steel|Cold_Steel|Titanium)_Ingot|.*?Shotgun_Shell.*?|.*_Arrow|.*_Bolt|.*_Fertilizer_?\\d*|.*_Grenade|.*_Pill|.*_Tonic|Aluminum|Ammo_Casing|Animal_Fat|Carbon_Fiber|Composites|Concrete_Mix|Cured_Leather_?\\d?|Electronics|Epoxy_?\\d?|Glass\\d?|Gunpowder\\w*|Health_.*|Titanium_Plate|Organic_Resin|Platinum_Sheath|Refined_[a-zA-Z]+|Rope|Shotgun_Casing|Steel_Bloom\\d?|Tree_Sap\\w*"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    -- Ingot pattern for recipes that get count set to 1
 | 
					 | 
				
			||||||
    local ingotPattern = "(?-s)(Platinum|Steel|Cold_Steel|Titanium)_Ingot|Aluminum|Refined_[a-zA-Z]+|Glass\\d?"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    local factor = 16
 | 
					 | 
				
			||||||
    local bonus = 0.5
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for _, row in ipairs(data.Rows) do
 | 
					 | 
				
			||||||
      local recipeName = row.Name
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      -- Special case: Biofuel recipes - remove Tree_Sap input
 | 
					 | 
				
			||||||
      if string.find(recipeName, "Biofuel") then
 | 
					 | 
				
			||||||
        if row.Inputs then
 | 
					 | 
				
			||||||
          for i = #row.Inputs, 1, -1 do
 | 
					 | 
				
			||||||
            local input = row.Inputs[i]
 | 
					 | 
				
			||||||
            if input.Element and input.Element.RowName and string.find(input.Element.RowName, "Tree_Sap") then
 | 
					 | 
				
			||||||
              table.remove(row.Inputs, i)
 | 
					 | 
				
			||||||
              print("Removing input 'Tree_Sap' from processor recipe '" .. recipeName .. "'")
 | 
					 | 
				
			||||||
            end
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      -- Ingot recipes: set input and output counts to 1
 | 
					 | 
				
			||||||
      if matchesPattern(recipeName, ingotPattern) then
 | 
					 | 
				
			||||||
        if row.Inputs then
 | 
					 | 
				
			||||||
          for _, input in ipairs(row.Inputs) do
 | 
					 | 
				
			||||||
            input.Count = 1
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
        if row.Outputs then
 | 
					 | 
				
			||||||
          for _, output in ipairs(row.Outputs) do
 | 
					 | 
				
			||||||
            output.Count = 1
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      -- Selected recipes: multiply inputs by factor, outputs by factor * (1 + bonus)
 | 
					 | 
				
			||||||
      if matchesPattern(recipeName, selectionPattern) then
 | 
					 | 
				
			||||||
        if row.Inputs then
 | 
					 | 
				
			||||||
          for _, input in ipairs(row.Inputs) do
 | 
					 | 
				
			||||||
            local oldCount = input.Count
 | 
					 | 
				
			||||||
            input.Count = input.Count * factor
 | 
					 | 
				
			||||||
            print("Recipe " .. recipeName .. " Input.Count: " .. oldCount .. " -> " .. input.Count)
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if row.Outputs then
 | 
					 | 
				
			||||||
          for _, output in ipairs(row.Outputs) do
 | 
					 | 
				
			||||||
            local oldCount = output.Count
 | 
					 | 
				
			||||||
            output.Count = math.floor(output.Count * factor * (1 + bonus))
 | 
					 | 
				
			||||||
            print("Recipe " .. recipeName .. " Output.Count: " .. oldCount .. " -> " .. output.Count)
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
`,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	commands, err := ProcessJSON(content, command, "test.json")
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("ProcessJSON failed: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(commands) == 0 {
 | 
					 | 
				
			||||||
		t.Fatal("Expected at least one command")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Apply the commands
 | 
					 | 
				
			||||||
	result := content
 | 
					 | 
				
			||||||
	for _, cmd := range commands {
 | 
					 | 
				
			||||||
		result = result[:cmd.From] + cmd.With + result[cmd.To:]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	diff := cmp.Diff(result, expected)
 | 
					 | 
				
			||||||
	if diff != "" {
 | 
					 | 
				
			||||||
		t.Errorf("Differences:\n%s", diff)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check that the result matches expected (preserves formatting and changes weight)
 | 
					 | 
				
			||||||
	if result != expected {
 | 
					 | 
				
			||||||
		t.Errorf("Expected:\n%s\n\nGot:\n%s", expected, result)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -609,239 +260,24 @@ func TestRetardedJSONEditing(t *testing.T) {
 | 
				
			|||||||
		result = result[:cmd.From] + cmd.With + result[cmd.To:]
 | 
							result = result[:cmd.From] + cmd.With + result[cmd.To:]
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	diff := cmp.Diff(result, expected)
 | 
					 | 
				
			||||||
	if diff != "" {
 | 
					 | 
				
			||||||
		t.Errorf("Differences:\n%s", diff)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check that the weight was changed
 | 
						// Check that the weight was changed
 | 
				
			||||||
	if result != expected {
 | 
						if result != expected {
 | 
				
			||||||
		t.Errorf("Expected:\n%s\nGot:\n%s", expected, result)
 | 
							t.Errorf("Expected:\n%s\nGot:\n%s", expected, result)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestRetardedJSONEditing2(t *testing.T) {
 | 
					func contains(s, substr string) bool {
 | 
				
			||||||
	original := `
 | 
						return len(s) >= len(substr) && (s == substr ||
 | 
				
			||||||
  {
 | 
							(len(s) > len(substr) && (s[:len(substr)] == substr ||
 | 
				
			||||||
      "Rows": [
 | 
								s[len(s)-len(substr):] == substr ||
 | 
				
			||||||
        {
 | 
								containsSubstring(s, substr))))
 | 
				
			||||||
            "Name": "Deep_Mining_Drill_Biofuel",
 | 
					}
 | 
				
			||||||
            "Meshable": {
 | 
					
 | 
				
			||||||
                "RowName": "Mesh_Deep_Mining_Drill_Biofuel"
 | 
					func containsSubstring(s, substr string) bool {
 | 
				
			||||||
            },
 | 
						for i := 0; i <= len(s)-len(substr); i++ {
 | 
				
			||||||
            "Itemable": {
 | 
							if s[i:i+len(substr)] == substr {
 | 
				
			||||||
                "RowName": "Item_Deep_Mining_Drill_Biofuel"
 | 
								return true
 | 
				
			||||||
            },
 | 
							}
 | 
				
			||||||
            "Interactable": {
 | 
						}
 | 
				
			||||||
                "RowName": "Deployable"
 | 
						return false
 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Focusable": {
 | 
					 | 
				
			||||||
                "RowName": "Focusable_1H"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Highlightable": {
 | 
					 | 
				
			||||||
                "RowName": "Generic"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Actionable": {
 | 
					 | 
				
			||||||
                "RowName": "Deployable"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Usable": {
 | 
					 | 
				
			||||||
                "RowName": "Place"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Deployable": {
 | 
					 | 
				
			||||||
                "RowName": "Deep_Mining_Drill_Biofuel"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Durable": {
 | 
					 | 
				
			||||||
                "RowName": "Deployable_750"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Inventory": {
 | 
					 | 
				
			||||||
                "RowName": "Deep_Mining_Drill_Biofuel"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Decayable": {
 | 
					 | 
				
			||||||
                "RowName": "Decay_MetaItem"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Generator": {
 | 
					 | 
				
			||||||
                "RowName": "Deep_Mining_Biofuel_Drill"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Resource": {
 | 
					 | 
				
			||||||
                "RowName": "Simple_Internal_Flow_Only"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Manual_Tags": {
 | 
					 | 
				
			||||||
                "GameplayTags": [
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Item.Machine"
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Generated_Tags": {
 | 
					 | 
				
			||||||
                "GameplayTags": [
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Item.Machine"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Meshable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Itemable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Interactable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Highlightable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Actionable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Usable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Deployable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Durable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Inventory"
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                "ParentTags": []
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  `
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	expected := `
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
      "Rows": [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            "Name": "Deep_Mining_Drill_Biofuel",
 | 
					 | 
				
			||||||
            "Meshable": {
 | 
					 | 
				
			||||||
                "RowName": "Mesh_Deep_Mining_Drill_Biofuel"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Itemable": {
 | 
					 | 
				
			||||||
                "RowName": "Item_Deep_Mining_Drill_Biofuel"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Interactable": {
 | 
					 | 
				
			||||||
                "RowName": "Deployable"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Focusable": {
 | 
					 | 
				
			||||||
                "RowName": "Focusable_1H"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Highlightable": {
 | 
					 | 
				
			||||||
                "RowName": "Generic"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Actionable": {
 | 
					 | 
				
			||||||
                "RowName": "Deployable"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Usable": {
 | 
					 | 
				
			||||||
                "RowName": "Place"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Deployable": {
 | 
					 | 
				
			||||||
                "RowName": "Deep_Mining_Drill_Biofuel"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Durable": {
 | 
					 | 
				
			||||||
                "RowName": "Deployable_750"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Inventory": {
 | 
					 | 
				
			||||||
                "RowName": "Deep_Mining_Drill_Biofuel"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Decayable": {
 | 
					 | 
				
			||||||
                "RowName": "Decay_MetaItem"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Generator": {
 | 
					 | 
				
			||||||
                "RowName": "Deep_Mining_Biofuel_Drill"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Resource": {
 | 
					 | 
				
			||||||
                "RowName": "Simple_Internal_Flow_Only"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Manual_Tags": {
 | 
					 | 
				
			||||||
                "GameplayTags": [
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Item.Machine"
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "Generated_Tags": {
 | 
					 | 
				
			||||||
                "GameplayTags": [
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Item.Machine"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Meshable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Itemable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Interactable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Highlightable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Actionable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Usable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Deployable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Durable"
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "TagName": "Traits.Inventory"
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                "ParentTags": []
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        ,"AdditionalStats": {"(Value=\"BaseDeepMiningDrillSpeed_+%\")":4000}}
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  `
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	command := utils.ModifyCommand{
 | 
					 | 
				
			||||||
		Name: "test",
 | 
					 | 
				
			||||||
		Lua: `
 | 
					 | 
				
			||||||
    for i, row in ipairs(data.Rows) do
 | 
					 | 
				
			||||||
      -- Special case: Deep_Mining_Drill_Biofuel
 | 
					 | 
				
			||||||
      if string.find(row.Name, "Deep_Mining_Drill_Biofuel") then
 | 
					 | 
				
			||||||
        print("[DEBUG]  Special case: Deep_Mining_Drill_Biofuel")
 | 
					 | 
				
			||||||
        if not row.AdditionalStats then
 | 
					 | 
				
			||||||
          print("[DEBUG]   Creating AdditionalStats table for Deep_Mining_Drill_Biofuel")
 | 
					 | 
				
			||||||
          row.AdditionalStats = {}
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
        print("[DEBUG]   Setting BaseDeepMiningDrillSpeed_+% to 4000")
 | 
					 | 
				
			||||||
        row.AdditionalStats["(Value=\\\"BaseDeepMiningDrillSpeed_+%\\\")"] = 4000
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
`,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	commands, err := ProcessJSON(original, command, "test.json")
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		t.Fatalf("ProcessJSON failed: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(commands) == 0 {
 | 
					 | 
				
			||||||
		t.Fatal("Expected at least one command")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Apply the commands
 | 
					 | 
				
			||||||
	result := original
 | 
					 | 
				
			||||||
	for _, cmd := range commands {
 | 
					 | 
				
			||||||
		result = result[:cmd.From] + cmd.With + result[cmd.To:]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	diff := cmp.Diff(result, expected)
 | 
					 | 
				
			||||||
	if diff != "" {
 | 
					 | 
				
			||||||
		t.Errorf("Differences:\n%s", diff)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if result != expected {
 | 
					 | 
				
			||||||
		t.Errorf("Expected:\n%s\nGot:\n%s", expected, result)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										25
									
								
								utils/db.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								utils/db.go
									
									
									
									
									
								
							@@ -1,7 +1,6 @@
 | 
				
			|||||||
package utils
 | 
					package utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,25 +41,24 @@ func GetDB() (DB, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	dbFile := filepath.Join("data.sqlite")
 | 
						dbFile := filepath.Join("data.sqlite")
 | 
				
			||||||
	getDBLogger.Debug("Opening database file: %q", dbFile)
 | 
						getDBLogger.Debug("Opening database file: %q", dbFile)
 | 
				
			||||||
	getDBLogger.Trace("Database configuration: PrepareStmt=true, GORM logger=Silent")
 | 
					 | 
				
			||||||
	db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{
 | 
						db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{
 | 
				
			||||||
		// SkipDefaultTransaction: true,
 | 
							// SkipDefaultTransaction: true,
 | 
				
			||||||
		PrepareStmt: true,
 | 
							PrepareStmt: true,
 | 
				
			||||||
		Logger:      gormlogger.Default.LogMode(gormlogger.Silent),
 | 
							Logger:      gormlogger.Default.LogMode(gormlogger.Silent),
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		getDBLogger.Error("Failed to open database file %q: %v", dbFile, err)
 | 
							getDBLogger.Error("Failed to open database: %v", err)
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	getDBLogger.Debug("Database opened successfully, running auto migration for FileSnapshot model")
 | 
						getDBLogger.Debug("Database opened successfully, running auto migration")
 | 
				
			||||||
	if err := db.AutoMigrate(&FileSnapshot{}); err != nil {
 | 
						if err := db.AutoMigrate(&FileSnapshot{}); err != nil {
 | 
				
			||||||
		getDBLogger.Error("Auto migration failed for FileSnapshot model: %v", err)
 | 
							getDBLogger.Error("Auto migration failed: %v", err)
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	getDBLogger.Info("Database initialized and migrated successfully")
 | 
						getDBLogger.Debug("Auto migration completed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	globalDB = &DBWrapper{db: db}
 | 
						globalDB = &DBWrapper{db: db}
 | 
				
			||||||
	getDBLogger.Debug("Database wrapper initialized and cached globally")
 | 
						getDBLogger.Debug("Database wrapper initialized")
 | 
				
			||||||
	return globalDB, nil
 | 
						return globalDB, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -90,7 +88,7 @@ func (db *DBWrapper) FileExists(filePath string) (bool, error) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (db *DBWrapper) SaveFile(filePath string, fileData []byte) error {
 | 
					func (db *DBWrapper) SaveFile(filePath string, fileData []byte) error {
 | 
				
			||||||
	saveFileLogger := dbLogger.WithPrefix("SaveFile").WithField("filePath", filePath).WithField("dataSize", len(fileData))
 | 
						saveFileLogger := dbLogger.WithPrefix("SaveFile").WithField("filePath", filePath)
 | 
				
			||||||
	saveFileLogger.Debug("Attempting to save file to database")
 | 
						saveFileLogger.Debug("Attempting to save file to database")
 | 
				
			||||||
	saveFileLogger.Trace("File data length: %d", len(fileData))
 | 
						saveFileLogger.Trace("File data length: %d", len(fileData))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -100,7 +98,7 @@ func (db *DBWrapper) SaveFile(filePath string, fileData []byte) error {
 | 
				
			|||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if exists {
 | 
						if exists {
 | 
				
			||||||
		saveFileLogger.Debug("File already exists in database, skipping save to avoid overwriting original snapshot")
 | 
							saveFileLogger.Debug("File already exists, skipping save")
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	saveFileLogger.Debug("Creating new file snapshot in database")
 | 
						saveFileLogger.Debug("Creating new file snapshot in database")
 | 
				
			||||||
@@ -112,7 +110,7 @@ func (db *DBWrapper) SaveFile(filePath string, fileData []byte) error {
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		saveFileLogger.Error("Failed to create file snapshot: %v", err)
 | 
							saveFileLogger.Error("Failed to create file snapshot: %v", err)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		saveFileLogger.Info("File successfully saved to database")
 | 
							saveFileLogger.Debug("File saved successfully to database")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -123,11 +121,8 @@ func (db *DBWrapper) GetFile(filePath string) ([]byte, error) {
 | 
				
			|||||||
	var fileSnapshot FileSnapshot
 | 
						var fileSnapshot FileSnapshot
 | 
				
			||||||
	err := db.db.Model(&FileSnapshot{}).Where("file_path = ?", filePath).First(&fileSnapshot).Error
 | 
						err := db.db.Model(&FileSnapshot{}).Where("file_path = ?", filePath).First(&fileSnapshot).Error
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
							// Downgrade not-found to warning to avoid noisy errors during first run
 | 
				
			||||||
			getFileLogger.Debug("File not found in database: %v", err)
 | 
							getFileLogger.Warning("Failed to get file from database: %v", err)
 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			getFileLogger.Warning("Failed to get file from database: %v", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	getFileLogger.Debug("File found in database")
 | 
						getFileLogger.Debug("File found in database")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,6 @@ var (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					func init() {
 | 
				
			||||||
	flagsLogger.Debug("Initializing command-line flags")
 | 
						flagsLogger.Debug("Initializing flags")
 | 
				
			||||||
	flagsLogger.Trace("Initial flag values - ParallelFiles: %d, Filter: %q, JSON: %t", *ParallelFiles, *Filter, *JSON)
 | 
						flagsLogger.Trace("ParallelFiles initial value: %d, Filter initial value: %q, JSON initial value: %t", *ParallelFiles, *Filter, *JSON)
 | 
				
			||||||
	flagsLogger.Debug("Flag definitions: -P (parallel files), -f (filter), -json (JSON mode)")
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user