Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bcc958dda | |||
| 11f0bbee53 | |||
| c145ad0900 | |||
| e02c1f018f | |||
| 07fea6238f | |||
| 5f1fdfa6c1 |
4
go.mod
4
go.mod
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
14
main.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
43
processor/luahelper-test-regress.lua
Normal file
43
processor/luahelper-test-regress.lua
Normal 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!")
|
||||
@@ -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!")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)`)
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user