6 Commits

11 changed files with 259 additions and 199 deletions

4
go.mod
View File

@@ -12,7 +12,6 @@ require (
)
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/hexops/valast v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -22,7 +21,6 @@ require (
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
@@ -35,7 +33,9 @@ require (
)
require (
github.com/BurntSushi/toml v1.5.0
github.com/google/go-cmp v0.6.0
github.com/spf13/cobra v1.10.1
github.com/tidwall/gjson v1.18.0
gorm.io/driver/sqlite v1.6.0
)

View File

@@ -85,7 +85,7 @@ END`
// Run the isolate commands
result, err := RunIsolateCommands(association, "test.txt", testContent, false)
if err != nil && err != NothingToDo {
if err != nil && err != ErrNothingToDo {
t.Fatalf("Failed to run isolate commands: %v", err)
}
@@ -163,7 +163,7 @@ END_SECTION2`
// Run the isolate commands
result, err := RunIsolateCommands(associations["test.txt"], "test.txt", testContent, false)
if err != nil && err != NothingToDo {
if err != nil && err != ErrNothingToDo {
t.Fatalf("Failed to run isolate commands: %v", err)
}
@@ -235,7 +235,7 @@ func TestIsolateCommandsWithJSONMode(t *testing.T) {
// Run the isolate commands
result, err := RunIsolateCommands(associations["test.json"], "test.json", testContent, false)
if err != nil && err != NothingToDo {
if err != nil && err != ErrNothingToDo {
t.Fatalf("Failed to run isolate commands: %v", err)
}
@@ -310,7 +310,7 @@ END_REGULAR`
// First run isolate commands
isolateResult, err := RunIsolateCommands(association, "test.txt", testContent, false)
if err != nil && err != NothingToDo {
if err != nil && err != ErrNothingToDo {
t.Fatalf("Failed to run isolate commands: %v", err)
}
@@ -321,7 +321,7 @@ END_REGULAR`
// Then run regular commands
commandLoggers := make(map[string]*logger.Logger)
finalResult, err := RunOtherCommands("test.txt", isolateResult, association, commandLoggers, false)
if err != nil && err != NothingToDo {
if err != nil && err != ErrNothingToDo {
t.Fatalf("Failed to run regular commands: %v", err)
}
@@ -364,12 +364,12 @@ irons_spellbooks:chain_lightning
// Second command: targets all SpellPowerMultiplier with multiplier *4
commands := []utils.ModifyCommand{
{
Name: "healing",
Name: "healing",
Regexes: []string{
`irons_spellbooks:chain_creeper[\s\S]*?SpellPowerMultiplier = !num`,
`irons_spellbooks:chain_lightning[\s\S]*?SpellPowerMultiplier = !num`,
},
Lua: `v1 * 4`, // This should multiply by 4
Lua: `v1 * 4`, // This should multiply by 4
Files: []string{"irons_spellbooks-server.toml"},
Reset: true,
Isolate: true,
@@ -377,7 +377,7 @@ irons_spellbooks:chain_lightning
{
Name: "spellpower",
Regex: `SpellPowerMultiplier = !num`,
Lua: `v1 * 4`, // This should multiply by 4 again
Lua: `v1 * 4`, // This should multiply by 4 again
Files: []string{"irons_spellbooks-server.toml"},
Reset: true,
Isolate: true,
@@ -398,7 +398,7 @@ irons_spellbooks:chain_lightning
// Run the isolate commands
result, err := RunIsolateCommands(association, "irons_spellbooks-server.toml", testContent, false)
if err != nil && err != NothingToDo {
if err != nil && err != ErrNothingToDo {
t.Fatalf("Failed to run isolate commands: %v", err)
}
@@ -414,4 +414,4 @@ irons_spellbooks:chain_lightning
t.Logf("Original content:\n%s\n", testContent)
t.Logf("Result content:\n%s\n", result)
}
}

14
main.go
View File

@@ -340,23 +340,23 @@ func runModifier(args []string, cmd *cobra.Command) {
isChanged := false
mainLogger.Debug("Running isolate commands for file %q", file)
fileDataStr, err = RunIsolateCommands(association, file, fileDataStr, jsonFlag)
if err != nil && err != NothingToDo {
if err != nil && err != ErrNothingToDo {
mainLogger.Error("Failed to run isolate commands for file %q: %v", file, err)
atomic.AddInt64(&stats.FailedFiles, 1)
return
}
if err != NothingToDo {
if err != ErrNothingToDo {
isChanged = true
}
mainLogger.Debug("Running other commands for file %q", file)
fileDataStr, err = RunOtherCommands(file, fileDataStr, association, commandLoggers, jsonFlag)
if err != nil && err != NothingToDo {
if err != nil && err != ErrNothingToDo {
mainLogger.Error("Failed to run other commands for file %q: %v", file, err)
atomic.AddInt64(&stats.FailedFiles, 1)
return
}
if err != NothingToDo {
if err != ErrNothingToDo {
isChanged = true
}
@@ -474,7 +474,7 @@ func CreateExampleConfig() {
createExampleConfigLogger.Info("Wrote example_cook.toml")
}
var NothingToDo = errors.New("nothing to do")
var ErrNothingToDo = errors.New("nothing to do")
func RunOtherCommands(file string, fileDataStr string, association utils.FileCommandAssociation, commandLoggers map[string]*logger.Logger, jsonFlag bool) (string, error) {
runOtherCommandsLogger := mainLogger.WithPrefix("RunOtherCommands").WithField("file", file)
@@ -565,7 +565,7 @@ func RunOtherCommands(file string, fileDataStr string, association utils.FileCom
if len(modifications) == 0 {
runOtherCommandsLogger.Warning("No modifications found for file")
return fileDataStr, NothingToDo
return fileDataStr, ErrNothingToDo
}
runOtherCommandsLogger.Debug("Executing %d modifications for file", len(modifications))
@@ -663,7 +663,7 @@ func RunIsolateCommands(association utils.FileCommandAssociation, file string, f
}
if !anythingDone {
runIsolateCommandsLogger.Debug("No isolate modifications were made for file")
return fileDataStr, NothingToDo
return fileDataStr, ErrNothingToDo
}
return currentFileData, nil
}

View File

@@ -1,3 +1,5 @@
// Package processor provides JSON processing and Lua script execution capabilities
// for data transformation and manipulation.
package processor
import (
@@ -19,9 +21,9 @@ var jsonLogger = logger.Default.WithPrefix("processor/json")
// ProcessJSON applies Lua processing to JSON content
func ProcessJSON(content string, command utils.ModifyCommand, filename string) ([]utils.ReplaceCommand, error) {
processJsonLogger := jsonLogger.WithPrefix("ProcessJSON").WithField("commandName", command.Name).WithField("file", filename)
processJsonLogger.Debug("Starting JSON processing for file")
processJsonLogger.Trace("Initial file content length: %d", len(content))
processJSONLogger := jsonLogger.WithPrefix("ProcessJSON").WithField("commandName", command.Name).WithField("file", filename)
processJSONLogger.Debug("Starting JSON processing for file")
processJSONLogger.Trace("Initial file content length: %d", len(content))
var commands []utils.ReplaceCommand
startTime := time.Now()
@@ -30,15 +32,15 @@ func ProcessJSON(content string, command utils.ModifyCommand, filename string) (
var jsonData interface{}
err := json.Unmarshal([]byte(content), &jsonData)
if err != nil {
processJsonLogger.Error("Failed to parse JSON content: %v", err)
processJSONLogger.Error("Failed to parse JSON content: %v", err)
return commands, fmt.Errorf("failed to parse JSON: %v", err)
}
processJsonLogger.Debug("Successfully parsed JSON content")
processJSONLogger.Debug("Successfully parsed JSON content")
// Create Lua state
L, err := NewLuaState()
if err != nil {
processJsonLogger.Error("Error creating Lua state: %v", err)
processJSONLogger.Error("Error creating Lua state: %v", err)
return commands, fmt.Errorf("error creating Lua state: %v", err)
}
defer L.Close()
@@ -49,70 +51,58 @@ func ProcessJSON(content string, command utils.ModifyCommand, filename string) (
// Convert JSON data to Lua table
luaTable, err := ToLuaTable(L, jsonData)
if err != nil {
processJsonLogger.Error("Failed to convert JSON to Lua table: %v", err)
processJSONLogger.Error("Failed to convert JSON to Lua table: %v", err)
return commands, fmt.Errorf("failed to convert JSON to Lua table: %v", err)
}
// Set the JSON data as a global variable
L.SetGlobal("data", luaTable)
processJsonLogger.Debug("Set JSON data as Lua global 'data'")
processJSONLogger.Debug("Set JSON data as Lua global 'data'")
// Build and execute Lua script for JSON mode
luaExpr := BuildJSONLuaScript(command.Lua)
processJsonLogger.Debug("Built Lua script from expression: %q", command.Lua)
processJsonLogger.Trace("Full Lua script: %q", utils.LimitString(luaExpr, 200))
processJSONLogger.Debug("Built Lua script from expression: %q", command.Lua)
processJSONLogger.Trace("Full Lua script: %q", utils.LimitString(luaExpr, 200))
if err := L.DoString(luaExpr); err != nil {
processJsonLogger.Error("Lua script execution failed: %v\nScript: %s", err, utils.LimitString(luaExpr, 200))
processJSONLogger.Error("Lua script execution failed: %v\nScript: %s", err, utils.LimitString(luaExpr, 200))
return commands, fmt.Errorf("lua script execution failed: %v", err)
}
processJsonLogger.Debug("Lua script executed successfully")
processJSONLogger.Debug("Lua script executed successfully")
// Check if modification flag is set
modifiedVal := L.GetGlobal("modified")
if modifiedVal.Type() != lua.LTBool || !lua.LVAsBool(modifiedVal) {
processJsonLogger.Debug("Skipping - no modifications indicated by Lua script")
processJSONLogger.Debug("Skipping - no modifications indicated by Lua script")
return commands, nil
}
// Get the modified data from Lua
modifiedData := L.GetGlobal("data")
if modifiedData.Type() != lua.LTTable {
processJsonLogger.Error("Expected 'data' to be a table after Lua processing, got %s", modifiedData.Type().String())
processJSONLogger.Error("Expected 'data' to be a table after Lua processing, got %s", modifiedData.Type().String())
return commands, fmt.Errorf("expected 'data' to be a table after Lua processing")
}
// Convert back to Go interface
goData, err := FromLua(L, modifiedData)
if err != nil {
processJsonLogger.Error("Failed to convert Lua table back to Go: %v", err)
processJSONLogger.Error("Failed to convert Lua table back to Go: %v", err)
return commands, fmt.Errorf("failed to convert Lua table back to Go: %v", err)
}
processJsonLogger.Debug("About to call applyChanges with original data and modified data")
processJSONLogger.Debug("About to call applyChanges with original data and modified data")
commands, err = applyChanges(content, jsonData, goData)
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)
}
processJsonLogger.Debug("Total JSON processing time: %v", time.Since(startTime))
processJsonLogger.Debug("Generated %d total modifications", len(commands))
processJSONLogger.Debug("Total JSON processing time: %v", time.Since(startTime))
processJSONLogger.Debug("Generated %d total modifications", len(commands))
return commands, nil
}
// applyJSONChanges compares original and modified data and applies changes surgically
func applyJSONChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) {
var commands []utils.ReplaceCommand
appliedCommands, err := applyChanges(content, originalData, modifiedData)
if err == nil && len(appliedCommands) > 0 {
return appliedCommands, nil
}
return commands, fmt.Errorf("failed to make any changes to the json")
}
// applyChanges attempts to make surgical changes while preserving exact formatting
func applyChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) {
var commands []utils.ReplaceCommand
@@ -199,12 +189,10 @@ func applyChanges(content string, originalData, modifiedData interface{}) ([]uti
// 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,
@@ -437,8 +425,6 @@ func findDeepChanges(basePath string, original, modified interface{}) map[string
}
changes[currentPath] = nil // Mark for removal
}
} else {
// Elements added - more complex, skip for now
}
} else {
// Same length - check individual elements for value changes

View File

@@ -0,0 +1,43 @@
-- Load the helper script
dofile("luahelper.lua")
-- Test helper function
local function assert(condition, message)
if not condition then error("ASSERTION FAILED: " .. (message or "unknown error")) end
end
local function test(name, fn)
local ok, err = pcall(fn)
if ok then
print("PASS: " .. name)
else
print("FAIL: " .. name .. " - " .. tostring(err))
end
end
test("regression test 001", function()
local csv =
[[Id Enabled ModuleId DepartmentId IsDepartment PositionInGraph Parents Modifiers UpgradePrice
news_department TRUE navigation TRUE 2 0 NewsAnalyticsDepartment + 1 communication_relay communication_relay
nd_charge_bonus TRUE navigation news_department FALSE 1 0 news_department NDSkillChargeBonus + 1 expert_disk expert_disk
nd_cooldown_time_reduce TRUE navigation news_department FALSE 3 0 news_department NDCooldownTimeReduce - 2 communication_relay communication_relay]]
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
assert(#rows == 3, "Should have 3 rows")
assert(rows[1].Id == "news_department", "First row Id should be 'news_department'")
assert(rows[1].Enabled == "TRUE", "First row Enabled should be 'TRUE'")
assert(rows[1].ModuleId == "navigation", "First row ModuleId should be 'navigation'")
assert(rows[1].DepartmentId == "", "First row DepartmentId should be ''")
assert(rows[1].IsDepartment == "TRUE", "First row IsDepartment should be 'TRUE'")
assert(rows.Headers[1] == "Id", "First row Headers should be 'Id'")
assert(rows.Headers[2] == "Enabled", "First row Headers should be 'Enabled'")
assert(rows.Headers[3] == "ModuleId", "First row Headers should be 'ModuleId'")
assert(rows.Headers[4] == "DepartmentId", "First row Headers should be 'DepartmentId'")
assert(rows.Headers[5] == "IsDepartment", "First row Headers should be 'IsDepartment'")
assert(rows.Headers[6] == "PositionInGraph", "First row Headers should be 'PositionInGraph'")
assert(rows.Headers[7] == "Parents", "First row Headers should be 'Parents'")
assert(rows.Headers[8] == "Modifiers", "First row Headers should be 'Modifiers'")
assert(rows.Headers[9] == "UpgradePrice", "First row Headers should be 'UpgradePrice'")
end)
print("\nAll tests completed!")

View File

@@ -18,26 +18,23 @@ end
-- Test fromCSV option validation
test("fromCSV invalid option", function()
local csv = "a,b,c\n1,2,3"
local rows, err = fromCSV(csv, { invalidOption = true })
assert(rows ~= nil and #rows == 0, "Should return empty table on error")
assert(err ~= nil, "Should return error message")
assert(string.find(err, "unknown option"), "Error should mention unknown option")
local ok, errMsg = pcall(function() fromCSV(csv, { invalidOption = true }) end)
assert(ok == false, "Should raise error")
assert(string.find(errMsg, "unknown option"), "Error should mention unknown option")
end)
-- Test toCSV error handling
-- Test toCSV invalid delimiter
test("toCSV invalid delimiter", function()
local rows = { { "a", "b", "c" } }
local csv, err = toCSV(rows, { delimiter = 123 })
local csv = toCSV(rows, { delimiter = 123 })
-- toCSV converts delimiter to string, so 123 becomes "123"
assert(csv == "a123b123c", "Should convert delimiter to string")
assert(err == nil, "Should not return error")
end)
-- Test fromCSV basic parsing
test("fromCSV basic", function()
local csv = "a,b,c\n1,2,3\n4,5,6"
local rows, err = fromCSV(csv)
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv)
assert(#rows == 3, "Should have 3 rows")
assert(rows[1][1] == "a", "First row first field should be 'a'")
assert(rows[2][2] == "2", "Second row second field should be '2'")
@@ -46,8 +43,7 @@ end)
-- Test fromCSV with headers
test("fromCSV with headers", function()
local csv = "foo,bar,baz\n1,2,3\n4,5,6"
local rows, err = fromCSV(csv, { hasheader = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hasheader = true })
assert(#rows == 2, "Should have 2 data rows")
assert(rows[1][1] == "1", "First row first field should be '1'")
assert(rows[1].foo == "1", "First row foo should be '1'")
@@ -58,8 +54,7 @@ end)
-- Test fromCSV with custom delimiter
test("fromCSV with tab delimiter", function()
local csv = "a\tb\tc\n1\t2\t3"
local rows, err = fromCSV(csv, { delimiter = "\t" })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t" })
assert(#rows == 2, "Should have 2 rows")
assert(rows[1][1] == "a", "First row first field should be 'a'")
assert(rows[2][2] == "2", "Second row second field should be '2'")
@@ -68,8 +63,7 @@ end)
-- Test fromCSV with quoted fields
test("fromCSV with quoted fields", function()
local csv = '"hello,world","test"\n"foo","bar"'
local rows, err = fromCSV(csv)
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv)
assert(#rows == 2, "Should have 2 rows")
assert(rows[1][1] == "hello,world", "Quoted field with comma should be preserved")
assert(rows[1][2] == "test", "Second field should be 'test'")
@@ -78,44 +72,37 @@ end)
-- Test toCSV basic
test("toCSV basic", function()
local rows = { { "a", "b", "c" }, { "1", "2", "3" } }
local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
local csv = toCSV(rows)
assert(csv == "a,b,c\n1,2,3", "CSV output should match expected")
end)
-- Test toCSV with custom delimiter
test("toCSV with tab delimiter", function()
local rows = { { "a", "b", "c" }, { "1", "2", "3" } }
local csv, err = toCSV(rows, { delimiter = "\t" })
if err then error("toCSV error: " .. err) end
local csv = toCSV(rows, { delimiter = "\t" })
assert(csv == "a\tb\tc\n1\t2\t3", "TSV output should match expected")
end)
-- Test toCSV with fields needing quoting
test("toCSV with quoted fields", function()
local rows = { { "hello,world", "test" }, { "foo", "bar" } }
local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
local csv = toCSV(rows)
assert(csv == '"hello,world",test\nfoo,bar', "Fields with commas should be quoted")
end)
-- Test round trip
test("fromCSV toCSV round trip", function()
local original = "a,b,c\n1,2,3\n4,5,6"
local rows, err = fromCSV(original)
if err then error("fromCSV error: " .. err) end
local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
local rows = fromCSV(original)
local csv = toCSV(rows)
assert(csv == original, "Round trip should preserve original")
end)
-- Test round trip with headers
test("fromCSV toCSV round trip with headers", function()
local original = "foo,bar,baz\n1,2,3\n4,5,6"
local rows, err = fromCSV(original, { hasheader = true })
if err then error("fromCSV error: " .. err) end
local csv, err = toCSV(rows)
if err then error("toCSV error: " .. err) end
local rows = fromCSV(original, { hasheader = true })
local csv = toCSV(rows)
local expected = "1,2,3\n4,5,6"
assert(csv == expected, "Round trip with headers should preserve data rows")
end)
@@ -123,8 +110,7 @@ end)
-- Test fromCSV with comments
test("fromCSV with comments", function()
local csv = "# This is a comment\nfoo,bar,baz\n1,2,3\n# Another comment\n4,5,6"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 3, "Should have 3 rows (comments filtered, header + 2 data rows)")
assert(rows[1][1] == "foo", "First row should be header row")
assert(rows[2][1] == "1", "Second row first field should be '1'")
@@ -134,8 +120,7 @@ end)
-- Test fromCSV with comments and headers
test("fromCSV with comments and headers", function()
local csv = "#mercenary_profiles\nId,Name,Value\n1,Test,100\n# End of data\n2,Test2,200"
local rows, err = fromCSV(csv, { hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hasheader = true, hascomments = true })
assert(#rows == 2, "Should have 2 data rows")
assert(rows[1].Id == "1", "First row Id should be '1'")
assert(rows[1].Name == "Test", "First row Name should be 'Test'")
@@ -146,8 +131,7 @@ end)
-- Test fromCSV with comments disabled
test("fromCSV without comments", function()
local csv = "# This should not be filtered\nfoo,bar\n1,2"
local rows, err = fromCSV(csv, { hascomments = false })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = false })
assert(#rows == 3, "Should have 3 rows (including comment)")
assert(rows[1][1] == "# This should not be filtered", "Comment line should be preserved")
end)
@@ -155,8 +139,7 @@ end)
-- Test fromCSV with comment at start
test("fromCSV comment at start", function()
local csv = "# Header comment\nId,Name\n1,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (comment filtered)")
assert(rows[1][1] == "Id", "First row should be header")
end)
@@ -164,8 +147,7 @@ end)
-- Test fromCSV with comment with leading whitespace
test("fromCSV comment with whitespace", function()
local csv = " # Comment with spaces\nId,Name\n1,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (comment with spaces filtered)")
assert(rows[1][1] == "Id", "First row should be header")
end)
@@ -173,8 +155,7 @@ end)
-- Test fromCSV with comment with tabs
test("fromCSV comment with tabs", function()
local csv = "\t# Comment with tab\nId,Name\n1,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (comment with tab filtered)")
assert(rows[1][1] == "Id", "First row should be header")
end)
@@ -182,8 +163,7 @@ end)
-- Test fromCSV with multiple consecutive comments
test("fromCSV multiple consecutive comments", function()
local csv = "# First comment\n# Second comment\n# Third comment\nId,Name\n1,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (all comments filtered)")
assert(rows[1][1] == "Id", "First row should be header")
end)
@@ -191,8 +171,7 @@ end)
-- Test fromCSV with comment in middle of data
test("fromCSV comment in middle", function()
local csv = "Id,Name\n1,Test\n# Middle comment\n2,Test2"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 3, "Should have 3 rows (comment filtered)")
assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][1] == "1", "Second row should be first data")
@@ -202,8 +181,7 @@ end)
-- Test fromCSV with comment at end
test("fromCSV comment at end", function()
local csv = "Id,Name\n1,Test\n# End comment"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (end comment filtered)")
assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][1] == "1", "Second row should be data")
@@ -212,8 +190,7 @@ end)
-- Test fromCSV with empty comment line
test("fromCSV empty comment", function()
local csv = "#\nId,Name\n1,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 2, "Should have 2 rows (empty comment filtered)")
assert(rows[1][1] == "Id", "First row should be header")
end)
@@ -221,8 +198,7 @@ end)
-- Test fromCSV with comment and headers
test("fromCSV comment with headers enabled", function()
local csv = "#mercenary_profiles\nId,Name,Value\n1,Test,100\n2,Test2,200"
local rows, err = fromCSV(csv, { hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hasheader = true, hascomments = true })
assert(#rows == 2, "Should have 2 data rows")
assert(rows[1].Id == "1", "First row Id should be '1'")
assert(rows[1].Name == "Test", "First row Name should be 'Test'")
@@ -232,8 +208,7 @@ end)
-- Test fromCSV with comment and TSV delimiter
test("fromCSV comment with tab delimiter", function()
local csv = "# Comment\nId\tName\n1\tTest"
local rows, err = fromCSV(csv, { delimiter = "\t", hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hascomments = true })
assert(#rows == 2, "Should have 2 rows")
assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][1] == "1", "Second row first field should be '1'")
@@ -242,8 +217,7 @@ end)
-- Test fromCSV with comment and headers and TSV
test("fromCSV comment with headers and TSV", function()
local csv = "#mercenary_profiles\nId\tName\tValue\n1\tTest\t100"
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = true })
assert(#rows == 1, "Should have 1 data row")
assert(rows[1].Id == "1", "Row Id should be '1'")
assert(rows[1].Name == "Test", "Row Name should be 'Test'")
@@ -253,8 +227,7 @@ end)
-- Test fromCSV with data field starting with # (not a comment)
test("fromCSV data field starting with hash", function()
local csv = "Id,Name\n1,#NotAComment\n2,Test"
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 3, "Should have 3 rows (data with # not filtered)")
assert(rows[1][1] == "Id", "First row should be header")
assert(rows[2][2] == "#NotAComment", "Second row should have #NotAComment as data")
@@ -263,8 +236,7 @@ end)
-- Test fromCSV with quoted field starting with #
test("fromCSV quoted field with hash", function()
local csv = 'Id,Name\n1,"#NotAComment"\n2,Test'
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 3, "Should have 3 rows (quoted # not filtered)")
assert(rows[2][2] == "#NotAComment", "Quoted field with # should be preserved")
end)
@@ -272,8 +244,7 @@ end)
-- Test fromCSV with comment after quoted field
test("fromCSV comment after quoted field", function()
local csv = 'Id,Name\n1,"Test"\n# This is a comment\n2,Test2'
local rows, err = fromCSV(csv, { hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { hascomments = true })
assert(#rows == 3, "Should have 3 rows (comment filtered)")
assert(rows[2][2] == "Test", "Quoted field should be preserved")
assert(rows[3][1] == "2", "Third row should be second data row")
@@ -414,8 +385,7 @@ Id ModifyStartCost ModifyStep ModifyLevelLimit Health ResistSheet WoundSlots Mel
john_hawkwood_boss 20 0.1 140 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 8 16 crit 1.60 critchance 0.05 0.5 0.5 0.03 0.5 1.2 0.3 8 2200 16 2 talent_the_man_who_sold_the_world human_male 0 hair1 #633D08 player Human
francis_reid_daly 20 0.1 130 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 7 14 crit 1.70 critchance 0.05 0.5 0.4 0.04 0.9 1 0.3 8 2000 10 1 talent_weapon_durability human_male 0 player Human
]]
local rows, err = fromCSV(teststr, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(teststr, { delimiter = "\t", hasheader = true, hascomments = true })
assert(#rows == 2, "Should have 2 data rows")
-- Test first row
@@ -440,8 +410,7 @@ end)
test("fromCSV debug header assignment", function()
local csv = "Id Name Value\n1 Test 100\n2 Test2 200"
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
assert(rows[1].Id == "1", "Id should be '1'")
assert(rows[1].Name == "Test", "Name should be 'Test'")
assert(rows[1].Value == "100", "Value should be '100'")
@@ -453,8 +422,7 @@ Id ModifyStartCost ModifyStep ModifyLevelLimit Health ResistSheet WoundSlots Mel
john_hawkwood_boss 20 0.1 140 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 8 16 crit 1.60 critchance 0.05 0.5 0.5 0.03 0.5 1.2 0.3 8 2200 16 2 talent_the_man_who_sold_the_world human_male 0 hair1 #633D08 player Human
francis_reid_daly 20 0.1 130 blunt 0 pierce 0 lacer 0 fire 0 cold 0 poison 0 shock 0 beam 0 HumanHead HumanShoulder HumanArm HumanThigh HumanFeet HumanChest HumanBody HumanStomach HumanKnee blunt 7 14 crit 1.70 critchance 0.05 0.5 0.4 0.04 0.9 1 0.3 8 2000 10 1 talent_weapon_durability human_male 0 player Human
]]
local rows, err = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = true })
assert(#rows == 2, "Should have 2 data rows")
assert(rows[1].Id == "john_hawkwood_boss", "First row Id should be 'john_hawkwood_boss'")
@@ -491,17 +459,14 @@ phoenix_brigade 30 0.1 shielding_basic battle_physicist_basic reinforced_battery
]]
-- Parse with headers and comments
local rows, err = fromCSV(original, { delimiter = "\t", hasheader = true, hascomments = true })
if err then error("fromCSV error: " .. err) end
local rows = fromCSV(original, { delimiter = "\t", hasheader = true, hascomments = true })
assert(#rows > 0, "Should have parsed rows")
-- Convert back to CSV with headers
local csv, err = toCSV(rows, { delimiter = "\t", hasheader = true })
if err then error("toCSV error: " .. err) end
local csv = toCSV(rows, { delimiter = "\t", hasheader = true })
-- Parse again
local rows2, err = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = false })
if err then error("fromCSV error: " .. err) end
local rows2 = fromCSV(csv, { delimiter = "\t", hasheader = true, hascomments = false })
-- Verify identical - same number of rows
assert(#rows2 == #rows, "Round trip should have same number of rows")
@@ -515,9 +480,55 @@ phoenix_brigade 30 0.1 shielding_basic battle_physicist_basic reinforced_battery
assert(rows2[1].Health == rows[1].Health, "Round trip first row Health should match")
-- Verify headers are preserved
assert(rows2[1].Headers ~= nil, "Round trip rows should have Headers field")
assert(#rows2[1].Headers == #rows[1].Headers, "Headers should have same number of elements")
assert(rows2[1].Headers[1] == rows[1].Headers[1], "First header should match")
assert(rows2.Headers ~= nil, "Round trip rows should have Headers field")
assert(#rows2.Headers == #rows.Headers, "Headers should have same number of elements")
assert(rows2.Headers[1] == rows.Headers[1], "First header should match")
end)
-- Test metatable: row[1] and row.foobar return same value
test("metatable row[1] equals row.header", function()
local csv = "Id Name Value\n1 Test 100"
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
assert(rows[1][1] == rows[1].Id, "row[1] should equal row.Id")
assert(rows[1][2] == rows[1].Name, "row[2] should equal row.Name")
assert(rows[1][3] == rows[1].Value, "row[3] should equal row.Value")
assert(rows[1].Id == "1", "row.Id should be '1'")
assert(rows[1][1] == "1", "row[1] should be '1'")
end)
-- Test metatable: setting via header name updates numeric index
test("metatable set via header name", function()
local csv = "Id Name Value\n1 Test 100"
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
rows[1].Id = "999"
assert(rows[1][1] == "999", "Setting row.Id should update row[1]")
assert(rows[1].Id == "999", "row.Id should be '999'")
end)
-- Test metatable: error on unknown header assignment
test("metatable error on unknown header", function()
local csv = "Id Name Value\n1 Test 100"
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
local ok, errMsg = pcall(function() rows[1].UnknownHeader = "test" end)
assert(ok == false, "Should error on unknown header")
assert(string.find(errMsg, "unknown header"), "Error should mention unknown header")
end)
-- Test metatable: numeric indices still work
test("metatable numeric indices work", function()
local csv = "Id Name Value\n1 Test 100"
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
rows[1][1] = "999"
assert(rows[1].Id == "999", "Setting row[1] should update row.Id")
assert(rows[1][1] == "999", "row[1] should be '999'")
end)
-- Test metatable: numeric keys work normally
test("metatable numeric keys work", function()
local csv = "Id Name Value\n1 Test 100"
local rows = fromCSV(csv, { delimiter = "\t", hasheader = true })
rows[1][100] = "hundred"
assert(rows[1][100] == "hundred", "Numeric keys should work")
end)
print("\nAll tests completed!")

View File

@@ -58,12 +58,10 @@ parserDefaultOptions = { delimiter = ",", hasheader = false, hascomments = false
--- Validates options against a set of valid option keys.
--- @param options ParserOptions? The options table to validate
--- @return boolean #True if options are valid
--- @return string? #Error message if invalid, nil if valid
function areOptionsValid(options)
if options == nil then return true, nil end
if options == nil then return end
if type(options) ~= "table" then return false, "options must be a table" end
if type(options) ~= "table" then error("options must be a table") end
-- Build valid options list from validOptions table
local validOptionsStr = ""
@@ -73,12 +71,11 @@ function areOptionsValid(options)
for k, _ in pairs(options) do
if parserDefaultOptions[k] == nil then
return false,
error(
"unknown option: " .. tostring(k) .. " (valid options: " .. validOptionsStr .. ")"
)
end
end
return true, nil
end
--- Parses CSV text into rows and fields using a minimal RFC 4180 state machine.
@@ -99,13 +96,11 @@ end
--- @param csv string The CSV text to parse.
--- @param options ParserOptions? Options for the parser
--- @return table #A table (array) of rows; each row is a table with numeric indices and optionally header-named keys.
--- @return string? #Error message if parsing fails.
function fromCSV(csv, options)
if options == nil then options = {} end
-- Validate options
local isValid, err = areOptionsValid(options)
if not isValid then return {}, err end
areOptionsValid(options)
local delimiter = options.delimiter or parserDefaultOptions.delimiter
local hasheader = options.hasheader or parserDefaultOptions.hasheader
@@ -209,24 +204,52 @@ function fromCSV(csv, options)
if hasheader and #allRows > 0 then
local headers = allRows[1]
local headerMap = {}
for j = 1, #headers do
if headers[j] ~= nil and headers[j] ~= "" then
local headerName = trim(headers[j])
headerMap[headerName] = j
end
end
local header_mt = {
headers = headerMap,
__index = function(t, key)
local mt = getmetatable(t)
if type(key) == "string" and mt.headers and mt.headers[key] then
return rawget(t, mt.headers[key])
end
return rawget(t, key)
end,
__newindex = function(t, key, value)
local mt = getmetatable(t)
if type(key) == "string" and mt.headers then
if mt.headers[key] then
rawset(t, mt.headers[key], value)
else
error("unknown header: " .. tostring(key))
end
else
rawset(t, key, value)
end
end,
}
local rows = {}
for ii = 2, #allRows do
local row = {}
local dataRow = allRows[ii]
for j = 1, #dataRow do
row[j] = dataRow[j]
if headers[j] ~= nil and headers[j] ~= "" then
local headerName = trim(headers[j])
row[headerName] = dataRow[j]
end
end
row.Headers = headers
setmetatable(row, header_mt)
table.insert(rows, row)
end
return rows, nil
rows.Headers = headers
return rows
end
return allRows, nil
return allRows
end
--- Converts a table of rows back to CSV text format (RFC 4180 compliant).
@@ -241,23 +264,21 @@ end
---
--- @param rows table Array of rows, where each row is an array of field values.
--- @param options ParserOptions? Options for the parser
--- @return string? #CSV-formatted text, error string?
--- @return string? #Error message if conversion fails.
--- @return string #CSV-formatted text
function toCSV(rows, options)
if options == nil then options = {} end
-- Validate options
local isValid, err = areOptionsValid(options)
if not isValid then return nil, err end
areOptionsValid(options)
local delimiter = options.delimiter or parserDefaultOptions.delimiter
local includeHeaders = options.hasheader or parserDefaultOptions.hasheader
local rowStrings = {}
-- Include headers row if requested and available
if includeHeaders and #rows > 0 and rows[1].Headers ~= nil then
if includeHeaders and #rows > 0 and rows.Headers ~= nil then
local headerStrings = {}
for _, header in ipairs(rows[1].Headers) do
for _, header in ipairs(rows.Headers) do
local headerStr = tostring(header)
local needsQuoting = false
if
@@ -304,7 +325,7 @@ function toCSV(rows, options)
table.insert(rowStrings, table.concat(fieldStrings, delimiter))
end
return table.concat(rowStrings, "\n"), nil
return table.concat(rowStrings, "\n")
end
-- String to number conversion helper

View File

@@ -229,8 +229,8 @@ func BuildLuaScript(luaExpr string) string {
// BuildJSONLuaScript prepares a Lua expression for JSON mode
func BuildJSONLuaScript(luaExpr string) string {
buildJsonLuaScriptLogger := processorLogger.WithPrefix("BuildJSONLuaScript").WithField("inputLuaExpr", luaExpr)
buildJsonLuaScriptLogger.Debug("Building full Lua script for JSON mode from expression")
buildJSONLuaScriptLogger := processorLogger.WithPrefix("BuildJSONLuaScript").WithField("inputLuaExpr", luaExpr)
buildJSONLuaScriptLogger.Debug("Building full Lua script for JSON mode from expression")
// Perform $var substitutions from globalVariables
luaExpr = replaceVariables(luaExpr)
@@ -242,7 +242,7 @@ func BuildJSONLuaScript(luaExpr string) string {
local res = run()
modified = res == nil or res
`, luaExpr)
buildJsonLuaScriptLogger.Trace("Generated full JSON Lua script: %q", utils.LimitString(fullScript, 200))
buildJSONLuaScriptLogger.Trace("Generated full JSON Lua script: %q", utils.LimitString(fullScript, 200))
return fullScript
}

View File

@@ -22,9 +22,9 @@ type CaptureGroup struct {
Range [2]int
}
// ProcessContent applies regex replacement with Lua processing
// The filename here exists ONLY so we can pass it to the lua environment
// It's not used for anything else
// ProcessRegex applies regex replacement with Lua processing.
// The filename here exists ONLY so we can pass it to the lua environment.
// It's not used for anything else.
func ProcessRegex(content string, command utils.ModifyCommand, filename string) ([]utils.ReplaceCommand, error) {
processRegexLogger := regexLogger.WithPrefix("ProcessRegex").WithField("commandName", command.Name).WithField("file", filename)
processRegexLogger.Debug("Starting regex processing for file")
@@ -216,9 +216,6 @@ func ProcessRegex(content string, command utils.ModifyCommand, filename string)
}
if replacement == "" {
// Apply the modifications to the original match
replacement = matchContent
// Count groups that were actually modified
modifiedGroupsCount := 0
for _, capture := range updatedCaptureGroups {

View File

@@ -30,7 +30,7 @@ func normalizeWhitespace(s string) string {
return re.ReplaceAllString(strings.TrimSpace(s), " ")
}
func ApiAdaptor(content string, regex string, lua string) (string, int, int, error) {
func APIAdaptor(content string, regex string, lua string) (string, int, int, error) {
command := utils.ModifyCommand{
Regex: regex,
Lua: lua,
@@ -79,7 +79,7 @@ func TestSimpleValueMultiplication(t *testing.T) {
</item>
</config>`
result, mods, matches, err := ApiAdaptor(content, `(?s)<value>(\d+)</value>`, "v1 = v1*1.5")
result, mods, matches, err := APIAdaptor(content, `(?s)<value>(\d+)</value>`, "v1 = v1*1.5")
assert.NoError(t, err, "Error processing content: %v", err)
assert.Equal(t, 1, matches, "Expected 1 match, got %d", matches)
@@ -100,7 +100,7 @@ func TestShorthandNotation(t *testing.T) {
</item>
</config>`
result, mods, matches, err := ApiAdaptor(content, `(?s)<value>(\d+)</value>`, "v1*1.5")
result, mods, matches, err := APIAdaptor(content, `(?s)<value>(\d+)</value>`, "v1*1.5")
assert.NoError(t, err, "Error processing content: %v", err)
assert.Equal(t, 1, matches, "Expected 1 match, got %d", matches)
@@ -121,7 +121,7 @@ func TestShorthandNotationFloats(t *testing.T) {
</item>
</config>`
result, mods, matches, err := ApiAdaptor(content, `(?s)<value>(\d+\.\d+)</value>`, "v1*1.5")
result, mods, matches, err := APIAdaptor(content, `(?s)<value>(\d+\.\d+)</value>`, "v1*1.5")
assert.NoError(t, err, "Error processing content: %v", err)
assert.Equal(t, 1, matches, "Expected 1 match, got %d", matches)
@@ -146,7 +146,7 @@ func TestArrayNotation(t *testing.T) {
</prices>
</config>`
result, mods, matches, err := ApiAdaptor(content, `(?s)<price>(\d+)</price>`, "v1*2")
result, mods, matches, err := APIAdaptor(content, `(?s)<price>(\d+)</price>`, "v1*2")
assert.NoError(t, err, "Error processing content: %v", err)
assert.Equal(t, 3, matches, "Expected 3 matches, got %d", matches)
@@ -167,7 +167,7 @@ func TestMultipleNumericMatches(t *testing.T) {
<entry>400</entry>
</data>`
result, mods, matches, err := ApiAdaptor(content, `<entry>(\d+)</entry>`, "v1*2")
result, mods, matches, err := APIAdaptor(content, `<entry>(\d+)</entry>`, "v1*2")
assert.NoError(t, err, "Error processing content: %v", err)
assert.Equal(t, 3, matches, "Expected 3 matches, got %d", matches)
@@ -186,7 +186,7 @@ func TestMultipleStringMatches(t *testing.T) {
<name>Mary_modified</name>
</data>`
result, mods, matches, err := ApiAdaptor(content, `<name>([A-Za-z]+)</name>`, `s1 = s1 .. "_modified"`)
result, mods, matches, err := APIAdaptor(content, `<name>([A-Za-z]+)</name>`, `s1 = s1 .. "_modified"`)
assert.NoError(t, err, "Error processing content: %v", err)
assert.Equal(t, 2, matches, "Expected 2 matches, got %d", matches)
@@ -205,7 +205,7 @@ func TestStringUpperCase(t *testing.T) {
<user>MARY</user>
</users>`
result, mods, matches, err := ApiAdaptor(content, `<user>([A-Za-z]+)</user>`, `s1 = string.upper(s1)`)
result, mods, matches, err := APIAdaptor(content, `<user>([A-Za-z]+)</user>`, `s1 = string.upper(s1)`)
assert.NoError(t, err, "Error processing content: %v", err)
assert.Equal(t, 2, matches, "Expected 2 matches, got %d", matches)
@@ -224,7 +224,7 @@ func TestStringConcatenation(t *testing.T) {
<product>Banana_fruit</product>
</products>`
result, mods, matches, err := ApiAdaptor(content, `<product>([A-Za-z]+)</product>`, `s1 = s1 .. "_fruit"`)
result, mods, matches, err := APIAdaptor(content, `<product>([A-Za-z]+)</product>`, `s1 = s1 .. "_fruit"`)
assert.NoError(t, err, "Error processing content: %v", err)
assert.Equal(t, 2, matches, "Expected 2 matches, got %d", matches)
@@ -254,7 +254,7 @@ func TestDecimalValues(t *testing.T) {
regex := regexp.MustCompile(`(?s)<value>([0-9.]+)</value>.*?<multiplier>([0-9.]+)</multiplier>`)
luaExpr := BuildLuaScript("v1 = v1 * v2")
result, _, _, err := ApiAdaptor(content, regex.String(), luaExpr)
result, _, _, err := APIAdaptor(content, regex.String(), luaExpr)
assert.NoError(t, err, "Error processing content: %v", err)
normalizedModified := normalizeWhitespace(result)
@@ -282,7 +282,7 @@ func TestLuaMathFunctions(t *testing.T) {
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`)
luaExpr := BuildLuaScript("v1 = math.sqrt(v1)")
modifiedContent, _, _, err := ApiAdaptor(content, regex.String(), luaExpr)
modifiedContent, _, _, err := APIAdaptor(content, regex.String(), luaExpr)
assert.NoError(t, err, "Error processing content: %v", err)
normalizedModified := normalizeWhitespace(modifiedContent)
@@ -310,7 +310,7 @@ func TestDirectAssignment(t *testing.T) {
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`)
luaExpr := BuildLuaScript("=0")
modifiedContent, _, _, err := ApiAdaptor(content, regex.String(), luaExpr)
modifiedContent, _, _, err := APIAdaptor(content, regex.String(), luaExpr)
assert.NoError(t, err, "Error processing content: %v", err)
normalizedModified := normalizeWhitespace(modifiedContent)
@@ -369,7 +369,7 @@ func TestStringAndNumericOperations(t *testing.T) {
luaExpr := BuildLuaScript(tt.luaExpression)
// Process with our function
result, modCount, _, err := ApiAdaptor(tt.input, pattern, luaExpr)
result, modCount, _, err := APIAdaptor(tt.input, pattern, luaExpr)
assert.NoError(t, err, "Process function failed: %v", err)
// Check results
@@ -430,7 +430,7 @@ func TestEdgeCases(t *testing.T) {
luaExpr := BuildLuaScript(tt.luaExpression)
// Process with our function
result, modCount, _, err := ApiAdaptor(tt.input, pattern, luaExpr)
result, modCount, _, err := APIAdaptor(tt.input, pattern, luaExpr)
assert.NoError(t, err, "Process function failed: %v", err)
// Check results
@@ -453,7 +453,7 @@ func TestNamedCaptureGroups(t *testing.T) {
</item>
</config>`
result, mods, matches, err := ApiAdaptor(content, `(?s)<value>(?<amount>\d+)</value>`, "amount = amount * 2")
result, mods, matches, err := APIAdaptor(content, `(?s)<value>(?<amount>\d+)</value>`, "amount = amount * 2")
assert.NoError(t, err, "Error processing content: %v", err)
assert.Equal(t, 1, matches, "Expected 1 match, got %d", matches)
@@ -474,7 +474,7 @@ func TestNamedCaptureGroupsNum(t *testing.T) {
</item>
</config>`
result, mods, matches, err := ApiAdaptor(content, `(?s)<value>(?<amount>!num)</value>`, "amount = amount * 2")
result, mods, matches, err := APIAdaptor(content, `(?s)<value>(?<amount>!num)</value>`, "amount = amount * 2")
assert.NoError(t, err, "Error processing content: %v", err)
assert.Equal(t, 1, matches, "Expected 1 match, got %d", matches)
@@ -495,7 +495,7 @@ func TestMultipleNamedCaptureGroups(t *testing.T) {
<quantity>15</quantity>
</product>`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`(?s)<name>(?<prodName>[^<]+)</name>.*?<price>(?<prodPrice>\d+\.\d+)</price>.*?<quantity>(?<prodQty>\d+)</quantity>`,
`prodName = string.upper(prodName)
prodPrice = round(prodPrice + 8, 2)
@@ -518,7 +518,7 @@ func TestMixedIndexedAndNamedCaptures(t *testing.T) {
<data>VALUE</data>
</entry>`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`(?s)<id>(\d+)</id>.*?<data>(?<dataField>[^<]+)</data>`,
`v1 = v1 * 2
dataField = string.upper(dataField)`)
@@ -550,7 +550,7 @@ func TestComplexNestedNamedCaptures(t *testing.T) {
</contact>
</person>`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`(?s)<details>.*?<name>(?<fullName>[^<]+)</name>.*?<age>(?<age>\d+)</age>`,
`fullName = string.upper(fullName) .. " (" .. age .. ")"`)
@@ -571,7 +571,7 @@ func TestNamedCaptureWithVariableReadback(t *testing.T) {
<mana>300</mana>
</stats>`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`(?s)<health>(?<hp>\d+)</health>.*?<mana>(?<mp>\d+)</mana>`,
`hp = hp * 1.5
mp = mp * 1.5`)
@@ -587,7 +587,7 @@ func TestNamedCaptureWithSpecialCharsInName(t *testing.T) {
expected := `<data value="84" min="10" max="100" />`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`<data value="(?<val_1>\d+)"`,
`val_1 = val_1 * 2`)
@@ -602,7 +602,7 @@ func TestEmptyNamedCapture(t *testing.T) {
expected := `<tag attr="default" />`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`attr="(?<value>.*?)"`,
`value = value == "" and "default" or value`)
@@ -617,7 +617,7 @@ func TestMultipleNamedCapturesInSameLine(t *testing.T) {
expected := `<rect x="20" y="40" width="200" height="100" />`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`x="(?<x>\d+)" y="(?<y>\d+)" width="(?<w>\d+)" height="(?<h>\d+)"`,
`x = x * 2
y = y * 2
@@ -641,7 +641,7 @@ func TestConditionalNamedCapture(t *testing.T) {
<item status="inactive" count="10" />
`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`<item status="(?<status>[^"]+)" count="(?<count>\d+)"`,
`count = status == "active" and count * 2 or count`)
@@ -662,7 +662,7 @@ func TestLuaFunctionsOnNamedCaptures(t *testing.T) {
<user name="JANE SMITH" role="admin" />
`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`<user name="(?<name>[^"]+)" role="(?<role>[^"]+)"`,
`-- Capitalize first letters for regular users
if role == "user" then
@@ -692,7 +692,7 @@ func TestNamedCaptureWithMath(t *testing.T) {
<item price="19.99" quantity="3" total="59.97" />
`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`<item price="(?<price>\d+\.\d+)" quantity="(?<qty>\d+)"!any$`,
`-- Calculate and add total
replacement = string.format('<item price="%s" quantity="%s" total="%.2f" />',
@@ -712,7 +712,7 @@ func TestNamedCaptureWithGlobals(t *testing.T) {
expected := `<temp unit="F">77</temp>`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`<temp unit="(?<unit>[CF]?)">(?<value>\d+)</temp>`,
`if unit == "C" then
value = value * 9/5 + 32
@@ -739,7 +739,7 @@ func TestMixedDynamicAndNamedCaptures(t *testing.T) {
<color rgb="0,255,0" name="GREEN" hex="#00FF00" />
`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`<color rgb="(?<r>\d+),(?<g>\d+),(?<b>\d+)" name="(?<colorName>[^"]+)" />`,
`-- Uppercase the name
colorName = string.upper(colorName)
@@ -765,7 +765,7 @@ func TestNamedCapturesWithMultipleReferences(t *testing.T) {
expected := `<text format="uppercase" length="11">HELLO WORLD</text>`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`<text>(?<content>[^<]+)</text>`,
`local uppercaseContent = string.upper(content)
local contentLength = string.len(content)
@@ -783,7 +783,7 @@ func TestNamedCaptureWithJsonData(t *testing.T) {
expected := `<data>{"name":"JOHN","age":30}</data>`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`<data>(?<json>\{.*?\})</data>`,
`-- Parse JSON (simplified, assumes valid JSON)
local name = json:match('"name":"([^"]+)"')
@@ -813,7 +813,7 @@ func TestNamedCaptureInXML(t *testing.T) {
</product>
`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`(?s)<price currency="(?<currency>[^"]+)">(?<price>\d+\.\d+)</price>.*?<stock>(?<stock>\d+)</stock>`,
`-- Add 20% to price if USD
if currency == "USD" then
@@ -870,7 +870,7 @@ func TestComprehensiveNamedCaptures(t *testing.T) {
</products>
`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`(?s)<product sku="(?<sku>[^"]+)" status="(?<status>[^"]+)"[^>]*>\s*<name>(?<product_name>[^<]+)</name>\s*<price currency="(?<currency>[^"]+)">(?<price>\d+\.\d+)</price>\s*<quantity>(?<qty>\d+)</quantity>`,
`-- Only process in-stock items
if status == "in-stock" then
@@ -924,7 +924,7 @@ func TestVariousNamedCaptureFormats(t *testing.T) {
</data>
`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`<entry id="(?<id_num>\d+)" value="(?<val>\d+)"(?: status="(?<status>[^"]*)")? />`,
`-- Prefix the ID with "ID-"
id_num = "ID-" .. id_num
@@ -963,7 +963,7 @@ func TestSimpleNamedCapture(t *testing.T) {
expected := `<product name="WIDGET" price="19.99"/>`
result, mods, matches, err := ApiAdaptor(content,
result, mods, matches, err := APIAdaptor(content,
`name="(?<product_name>[^"]+)"`,
`product_name = string.upper(product_name)`)

View File

@@ -404,6 +404,7 @@ files = ["test.txt"
invalidFile := filepath.Join(tmpDir, "invalid.toml")
err = os.WriteFile(invalidFile, []byte(invalidTOML), 0644)
assert.NoError(t, err, "Should write invalid TOML file")
commands, err := utils.LoadCommandsFromTomlFiles("invalid.toml")
assert.Error(t, err, "Should return error for invalid TOML syntax")
@@ -418,6 +419,7 @@ files = ["test.txt"
// Test 3: Empty TOML file creates an error (this is expected behavior)
emptyFile := filepath.Join(tmpDir, "empty.toml")
err = os.WriteFile(emptyFile, []byte(""), 0644)
assert.NoError(t, err, "Should write empty TOML file")
commands, err = utils.LoadCommandsFromTomlFiles("empty.toml")
assert.Error(t, err, "Should return error for empty TOML file")
@@ -508,4 +510,4 @@ func TestYAMLToTOMLConversion(t *testing.T) {
assert.Equal(t, tomlData, originalTomlData, "TOML file content should be unchanged")
t.Logf("YAML to TOML conversion test completed successfully")
}
}