From bff7cc2a27c105ae6e208fda40451e3f0b4063ff Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Thu, 21 Aug 2025 20:39:35 +0200 Subject: [PATCH] Hallucinate a json mode implementation --- .gitignore | 1 + main.go | 150 ++++++++++++++++++++++------ processor/json.go | 216 +++++++++++++++++++++++++++++++++++++++++ processor/json_test.go | 117 ++++++++++++++++++++++ processor/processor.go | 20 ++++ utils/flags.go | 3 +- utils/modifycommand.go | 12 ++- 7 files changed, 485 insertions(+), 34 deletions(-) create mode 100644 processor/json.go create mode 100644 processor/json_test.go diff --git a/.gitignore b/.gitignore index 6640f57..0745d0d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.exe .qodo *.sqlite +testfiles diff --git a/main.go b/main.go index 18af82b..99d7cfe 100644 --- a/main.go +++ b/main.go @@ -44,9 +44,13 @@ func main() { fmt.Fprintf(os.Stderr, " Reset files to their original state\n") fmt.Fprintf(os.Stderr, " -loglevel string\n") fmt.Fprintf(os.Stderr, " Set logging level: ERROR, WARNING, INFO, DEBUG, TRACE (default \"INFO\")\n") + fmt.Fprintf(os.Stderr, " -json\n") + fmt.Fprintf(os.Stderr, " Enable JSON mode for processing JSON files\n") fmt.Fprintf(os.Stderr, "\nExamples:\n") fmt.Fprintf(os.Stderr, " Regex mode (default):\n") fmt.Fprintf(os.Stderr, " %s \"(\\\\d+)\" \"*1.5\" data.xml\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " JSON mode:\n") + fmt.Fprintf(os.Stderr, " %s -json data.json\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\nNote: v1, v2, etc. are used to refer to capture groups as numbers.\n") fmt.Fprintf(os.Stderr, " s1, s2, etc. are used to refer to capture groups as strings.\n") fmt.Fprintf(os.Stderr, " Helper functions: num(str) converts string to number, str(num) converts number to string\n") @@ -426,7 +430,7 @@ func CreateExampleConfig() { }, // Multiline regex example (DOTALL is auto-enabled). Captures numeric in nested XML. { - Name: "XMLNestedValueMultiply", + Name: "XMLNestedValueMultiply", Regex: `\s*\s*!any<\/name>\s*\s*(!num)<\/value>\s*\s*<\/item>`, Lua: `* $multiply`, Files: []string{"data/**/*.xml"}, @@ -439,8 +443,8 @@ func CreateExampleConfig() { `\s*\n\s*(?P!num)\s*\n\s*(?P!num)\s*\n\s*`, `\[block\]\nkey=(?P[A-Za-z_]+)\nvalue=(?P!num)`, }, - Lua: `if is_number(score) then score = score * 2 end; if is_number(val) then val = val * 3 end; return true`, - Files: []string{"examples/**/*.*"}, + Lua: `if is_number(score) then score = score * 2 end; if is_number(val) then val = val * 3 end; return true`, + Files: []string{"examples/**/*.*"}, LogLevel: "DEBUG", }, // Use equals operator shorthand and boolean variable @@ -452,19 +456,19 @@ func CreateExampleConfig() { }, // Demonstrate NoDedup to allow overlapping replacements { - Name: "OverlappingGroups", - Regex: `(?P!num)(?P!num)`, - Lua: `a = num(a) + 1; b = num(b) + 1; return true`, - Files: []string{"overlap/**/*.txt"}, + Name: "OverlappingGroups", + Regex: `(?P!num)(?P!num)`, + Lua: `a = num(a) + 1; b = num(b) + 1; return true`, + Files: []string{"overlap/**/*.txt"}, NoDedup: true, }, // Isolate command example operating on entire matched block { - Name: "IsolateUppercaseBlock", - Regex: `BEGIN\n(?P!any)\nEND`, - Lua: `block = upper(block); return true`, - Files: []string{"logs/**/*.log"}, - Isolate: true, + Name: "IsolateUppercaseBlock", + Regex: `BEGIN\n(?P!any)\nEND`, + Lua: `block = upper(block); return true`, + Files: []string{"logs/**/*.log"}, + Isolate: true, LogLevel: "TRACE", }, // Using !rep placeholder and arrays of files @@ -481,6 +485,25 @@ func CreateExampleConfig() { Lua: `key = $prefix .. key; return true`, Files: []string{"**/*.properties"}, }, + // JSON mode examples + { + Name: "JSONArrayMultiply", + JSON: true, + Lua: `for i, item in ipairs(data.items) do data.items[i].value = item.value * 2 end; return true`, + Files: []string{"data/**/*.json"}, + }, + { + Name: "JSONObjectUpdate", + JSON: true, + Lua: `data.version = "2.0.0"; data.enabled = true; return true`, + Files: []string{"config/**/*.json"}, + }, + { + Name: "JSONNestedModify", + JSON: true, + Lua: `if data.settings and data.settings.performance then data.settings.performance.multiplier = data.settings.performance.multiplier * 1.5 end; return true`, + Files: []string{"settings/**/*.json"}, + }, } data, err := yaml.Marshal(commands) @@ -506,11 +529,51 @@ func RunOtherCommands(file string, fileDataStr string, association utils.FileCom runOtherCommandsLogger.Debug("Running other commands for file") runOtherCommandsLogger.Trace("File data before modifications: %s", utils.LimitString(fileDataStr, 200)) - // Aggregate all the modifications and execute them + // Separate JSON and regex commands for different processing approaches + jsonCommands := []utils.ModifyCommand{} + regexCommands := []utils.ModifyCommand{} + + for _, command := range association.Commands { + if command.JSON || *utils.JSON { + jsonCommands = append(jsonCommands, command) + } else { + regexCommands = append(regexCommands, command) + } + } + + // Process JSON commands sequentially (each operates on the entire file) + for _, command := range jsonCommands { + cmdLogger := logger.Default + if cmdLog, ok := commandLoggers[command.Name]; ok { + cmdLogger = cmdLog + } + + cmdLogger.Debug("Processing file with JSON mode for command %q", command.Name) + newModifications, err := processor.ProcessJSON(fileDataStr, command, file) + if err != nil { + runOtherCommandsLogger.Error("Failed to process file with JSON command %q: %v", command.Name, err) + continue + } + + // Apply JSON modifications immediately + if len(newModifications) > 0 { + var count int + fileDataStr, count = utils.ExecuteModifications(newModifications, fileDataStr) + atomic.AddInt64(&stats.TotalModifications, int64(count)) + cmdLogger.Debug("Applied %d JSON modifications for command %q", count, command.Name) + } + + count, ok := stats.ModificationsPerCommand.Load(command.Name) + if !ok { + count = 0 + } + stats.ModificationsPerCommand.Store(command.Name, count.(int)+len(newModifications)) + } + + // Aggregate regex modifications and execute them modifications := []utils.ReplaceCommand{} numCommandsConsidered := 0 - for _, command := range association.Commands { - // Use command-specific logger if available, otherwise fall back to default logger + for _, command := range regexCommands { cmdLogger := logger.Default if cmdLog, ok := commandLoggers[command.Name]; ok { cmdLogger = cmdLog @@ -572,35 +635,62 @@ func RunIsolateCommands(association utils.FileCommandAssociation, file string, f anythingDone := false for _, isolateCommand := range association.IsolateCommands { - runIsolateCommandsLogger.Debug("Begin processing file with isolate command %q", isolateCommand.Regex) - patterns := isolateCommand.Regexes - if len(patterns) == 0 { - patterns = []string{isolateCommand.Regex} - } - for idx, pattern := range patterns { - tmpCmd := isolateCommand - tmpCmd.Regex = pattern - modifications, err := processor.ProcessRegex(fileDataStr, tmpCmd, file) + // Check if this isolate command should use JSON mode + if isolateCommand.JSON || *utils.JSON { + runIsolateCommandsLogger.Debug("Begin processing file with JSON isolate command %q", isolateCommand.Name) + modifications, err := processor.ProcessJSON(fileDataStr, isolateCommand, file) if err != nil { - runIsolateCommandsLogger.Error("Failed to process file with isolate command %q (pattern %d/%d): %v", isolateCommand.Name, idx+1, len(patterns), err) + runIsolateCommandsLogger.Error("Failed to process file with JSON isolate command %q: %v", isolateCommand.Name, err) continue } if len(modifications) == 0 { - runIsolateCommandsLogger.Debug("Isolate command %q produced no modifications (pattern %d/%d)", isolateCommand.Name, idx+1, len(patterns)) + runIsolateCommandsLogger.Debug("JSON isolate command %q produced no modifications", isolateCommand.Name) continue } anythingDone = true - runIsolateCommandsLogger.Debug("Executing %d isolate modifications for file", len(modifications)) - runIsolateCommandsLogger.Trace("Isolate modifications: %v", modifications) + runIsolateCommandsLogger.Debug("Executing %d JSON isolate modifications for file", len(modifications)) + runIsolateCommandsLogger.Trace("JSON isolate modifications: %v", modifications) var count int fileDataStr, count = utils.ExecuteModifications(modifications, fileDataStr) - runIsolateCommandsLogger.Trace("File data after isolate modifications: %s", utils.LimitString(fileDataStr, 200)) + runIsolateCommandsLogger.Trace("File data after JSON isolate modifications: %s", utils.LimitString(fileDataStr, 200)) atomic.AddInt64(&stats.TotalModifications, int64(count)) - runIsolateCommandsLogger.Info("Executed %d isolate modifications for file", count) + runIsolateCommandsLogger.Info("Executed %d JSON isolate modifications for file", count) + } else { + // Regular regex processing for isolate commands + runIsolateCommandsLogger.Debug("Begin processing file with isolate command %q", isolateCommand.Regex) + patterns := isolateCommand.Regexes + if len(patterns) == 0 { + patterns = []string{isolateCommand.Regex} + } + for idx, pattern := range patterns { + tmpCmd := isolateCommand + tmpCmd.Regex = pattern + modifications, err := processor.ProcessRegex(fileDataStr, tmpCmd, file) + if err != nil { + runIsolateCommandsLogger.Error("Failed to process file with isolate command %q (pattern %d/%d): %v", isolateCommand.Name, idx+1, len(patterns), err) + continue + } + + if len(modifications) == 0 { + runIsolateCommandsLogger.Debug("Isolate command %q produced no modifications (pattern %d/%d)", isolateCommand.Name, idx+1, len(patterns)) + continue + } + anythingDone = true + + runIsolateCommandsLogger.Debug("Executing %d isolate modifications for file", len(modifications)) + runIsolateCommandsLogger.Trace("Isolate modifications: %v", modifications) + var count int + fileDataStr, count = utils.ExecuteModifications(modifications, fileDataStr) + runIsolateCommandsLogger.Trace("File data after isolate modifications: %s", utils.LimitString(fileDataStr, 200)) + + atomic.AddInt64(&stats.TotalModifications, int64(count)) + + runIsolateCommandsLogger.Info("Executed %d isolate modifications for file", count) + } } } if !anythingDone { diff --git a/processor/json.go b/processor/json.go new file mode 100644 index 0000000..f9a11a6 --- /dev/null +++ b/processor/json.go @@ -0,0 +1,216 @@ +package processor + +import ( + "cook/utils" + "encoding/json" + "fmt" + "time" + + logger "git.site.quack-lab.dev/dave/cylogger" + lua "github.com/yuin/gopher-lua" +) + +// jsonLogger is a scoped logger for the processor/json package. +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)) + + var commands []utils.ReplaceCommand + startTime := time.Now() + + // Parse JSON content + var jsonData interface{} + err := json.Unmarshal([]byte(content), &jsonData) + if err != nil { + 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") + + // Create Lua state + L, err := NewLuaState() + if err != nil { + processJsonLogger.Error("Error creating Lua state: %v", err) + return commands, fmt.Errorf("error creating Lua state: %v", err) + } + defer L.Close() + + // Set filename global + L.SetGlobal("file", lua.LString(filename)) + + // 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) + 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'") + + // 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)) + + if err := L.DoString(luaExpr); err != nil { + 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") + + // 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") + 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()) + 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) + return commands, fmt.Errorf("failed to convert Lua table back to Go: %v", err) + } + + // Marshal back to JSON + modifiedJSON, err := json.MarshalIndent(goData, "", " ") + if err != nil { + processJsonLogger.Error("Failed to marshal modified data to JSON: %v", err) + return commands, fmt.Errorf("failed to marshal modified data to JSON: %v", err) + } + + // Create replacement command for the entire file + // For JSON mode, we always replace the entire content + commands = append(commands, utils.ReplaceCommand{ + From: 0, + To: len(content), + With: string(modifiedJSON), + }) + + processJsonLogger.Debug("Total JSON processing time: %v", time.Since(startTime)) + processJsonLogger.Debug("Generated %d total modifications", len(commands)) + return commands, nil +} + +// ToLuaTable converts a Go interface{} to a Lua table recursively +func ToLuaTable(L *lua.LState, data interface{}) (*lua.LTable, error) { + toLuaTableLogger := jsonLogger.WithPrefix("ToLuaTable") + toLuaTableLogger.Debug("Converting Go interface to Lua table") + toLuaTableLogger.Trace("Input data type: %T", data) + + switch v := data.(type) { + case map[string]interface{}: + toLuaTableLogger.Debug("Converting map to Lua table") + table := L.CreateTable(0, len(v)) + for key, value := range v { + luaValue, err := ToLuaValue(L, value) + if err != nil { + toLuaTableLogger.Error("Failed to convert map value for key %q: %v", key, err) + return nil, err + } + table.RawSetString(key, luaValue) + } + return table, nil + + case []interface{}: + toLuaTableLogger.Debug("Converting slice to Lua table") + table := L.CreateTable(len(v), 0) + for i, value := range v { + luaValue, err := ToLuaValue(L, value) + if err != nil { + toLuaTableLogger.Error("Failed to convert slice value at index %d: %v", i, err) + return nil, err + } + table.RawSetInt(i+1, luaValue) // Lua arrays are 1-indexed + } + return table, nil + + case string: + toLuaTableLogger.Debug("Converting string to Lua string") + return nil, fmt.Errorf("expected table or array, got string") + + case float64: + toLuaTableLogger.Debug("Converting float64 to Lua number") + return nil, fmt.Errorf("expected table or array, got number") + + case bool: + toLuaTableLogger.Debug("Converting bool to Lua boolean") + return nil, fmt.Errorf("expected table or array, got boolean") + + case nil: + toLuaTableLogger.Debug("Converting nil to Lua nil") + return nil, fmt.Errorf("expected table or array, got nil") + + default: + toLuaTableLogger.Error("Unsupported type for Lua table conversion: %T", v) + return nil, fmt.Errorf("unsupported type for Lua table conversion: %T", v) + } +} + +// ToLuaValue converts a Go interface{} to a Lua value +func ToLuaValue(L *lua.LState, data interface{}) (lua.LValue, error) { + toLuaValueLogger := jsonLogger.WithPrefix("ToLuaValue") + toLuaValueLogger.Debug("Converting Go interface to Lua value") + toLuaValueLogger.Trace("Input data type: %T", data) + + switch v := data.(type) { + case map[string]interface{}: + toLuaValueLogger.Debug("Converting map to Lua table") + table := L.CreateTable(0, len(v)) + for key, value := range v { + luaValue, err := ToLuaValue(L, value) + if err != nil { + toLuaValueLogger.Error("Failed to convert map value for key %q: %v", key, err) + return lua.LNil, err + } + table.RawSetString(key, luaValue) + } + return table, nil + + case []interface{}: + toLuaValueLogger.Debug("Converting slice to Lua table") + table := L.CreateTable(len(v), 0) + for i, value := range v { + luaValue, err := ToLuaValue(L, value) + if err != nil { + toLuaValueLogger.Error("Failed to convert slice value at index %d: %v", i, err) + return lua.LNil, err + } + table.RawSetInt(i+1, luaValue) // Lua arrays are 1-indexed + } + return table, nil + + case string: + toLuaValueLogger.Debug("Converting string to Lua string") + return lua.LString(v), nil + + case float64: + toLuaValueLogger.Debug("Converting float64 to Lua number") + return lua.LNumber(v), nil + + case bool: + toLuaValueLogger.Debug("Converting bool to Lua boolean") + return lua.LBool(v), nil + + case nil: + toLuaValueLogger.Debug("Converting nil to Lua nil") + return lua.LNil, nil + + default: + toLuaValueLogger.Error("Unsupported type for Lua value conversion: %T", v) + return lua.LNil, fmt.Errorf("unsupported type for Lua value conversion: %T", v) + } +} diff --git a/processor/json_test.go b/processor/json_test.go new file mode 100644 index 0000000..9d0a7bb --- /dev/null +++ b/processor/json_test.go @@ -0,0 +1,117 @@ +package processor + +import ( + "cook/utils" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProcessJSON(t *testing.T) { + tests := []struct { + name string + input string + luaExpression string + expectedOutput string + expectedMods int + }{ + { + name: "Basic JSON object modification", + input: `{"name": "test", "value": 42}`, + luaExpression: `data.value = data.value * 2; return true`, + expectedOutput: `{ + "name": "test", + "value": 84 +}`, + expectedMods: 1, + }, + { + name: "JSON array modification", + input: `{"items": [{"id": 1, "value": 10}, {"id": 2, "value": 20}]}`, + luaExpression: `for i, item in ipairs(data.items) do data.items[i].value = item.value * 1.5 end; return true`, + expectedOutput: `{ + "items": [ + { + "id": 1, + "value": 15 + }, + { + "id": 2, + "value": 30 + } + ] +}`, + expectedMods: 1, + }, + { + name: "JSON nested object modification", + input: `{"config": {"settings": {"enabled": false, "timeout": 30}}}`, + luaExpression: `data.config.settings.enabled = true; data.config.settings.timeout = 60; return true`, + expectedOutput: `{ + "config": { + "settings": { + "enabled": true, + "timeout": 60 + } + } +}`, + expectedMods: 1, + }, + { + name: "JSON no modification", + input: `{"name": "test", "value": 42}`, + luaExpression: `return false`, + expectedOutput: `{"name": "test", "value": 42}`, + expectedMods: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + command := utils.ModifyCommand{ + Name: tt.name, + JSON: true, + Lua: tt.luaExpression, + } + + modifications, err := ProcessJSON(tt.input, command, "test.json") + assert.NoError(t, err, "ProcessJSON failed: %v", err) + + if len(modifications) > 0 { + // Execute modifications + result, count := utils.ExecuteModifications(modifications, tt.input) + assert.Equal(t, tt.expectedMods, count, "Expected %d modifications, got %d", tt.expectedMods, count) + assert.Equal(t, tt.expectedOutput, result, "Expected output: %s, got: %s", tt.expectedOutput, result) + } else { + assert.Equal(t, 0, tt.expectedMods, "Expected no modifications but got some") + } + }) + } +} + +func TestToLuaValue(t *testing.T) { + L, err := NewLuaState() + if err != nil { + t.Fatalf("Failed to create Lua state: %v", err) + } + defer L.Close() + + tests := []struct { + name string + input interface{} + expected string + }{ + {"string", "hello", "hello"}, + {"number", 42.0, "42"}, + {"boolean", true, "true"}, + {"nil", nil, "nil"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ToLuaValue(L, tt.input) + assert.NoError(t, err) + assert.Equal(t, tt.expected, result.String()) + }) + } +} diff --git a/processor/processor.go b/processor/processor.go index d40b7d8..f85eb97 100644 --- a/processor/processor.go +++ b/processor/processor.go @@ -300,6 +300,26 @@ func BuildLuaScript(luaExpr string) string { return fullScript } +// 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") + + // Perform $var substitutions from globalVariables + luaExpr = replaceVariables(luaExpr) + + fullScript := fmt.Sprintf(` + function run() + %s + end + local res = run() + modified = res == nil or res + `, luaExpr) + buildJsonLuaScriptLogger.Trace("Generated full JSON Lua script: %q", utils.LimitString(fullScript, 200)) + + return fullScript +} + func replaceVariables(expr string) string { // $varName -> literal value varNameRe := regexp.MustCompile(`\$(\w+)`) diff --git a/utils/flags.go b/utils/flags.go index d76d56f..d11f761 100644 --- a/utils/flags.go +++ b/utils/flags.go @@ -12,9 +12,10 @@ var flagsLogger = logger.Default.WithPrefix("utils/flags") var ( ParallelFiles = flag.Int("P", 100, "Number of files to process in parallel") Filter = flag.String("f", "", "Filter commands before running them") + JSON = flag.Bool("json", false, "Enable JSON mode for processing JSON files") ) func init() { flagsLogger.Debug("Initializing flags") - flagsLogger.Trace("ParallelFiles initial value: %d, Filter initial value: %q", *ParallelFiles, *Filter) + flagsLogger.Trace("ParallelFiles initial value: %d, Filter initial value: %q, JSON initial value: %t", *ParallelFiles, *Filter, *JSON) } diff --git a/utils/modifycommand.go b/utils/modifycommand.go index 9937055..3d7a560 100644 --- a/utils/modifycommand.go +++ b/utils/modifycommand.go @@ -25,6 +25,7 @@ type ModifyCommand struct { Isolate bool `yaml:"isolate,omitempty"` NoDedup bool `yaml:"nodedup,omitempty"` Disabled bool `yaml:"disable,omitempty"` + JSON bool `yaml:"json,omitempty"` Modifiers map[string]interface{} `yaml:"modifiers,omitempty"` } @@ -33,10 +34,15 @@ type CookFile []ModifyCommand func (c *ModifyCommand) Validate() error { validateLogger := modifyCommandLogger.WithPrefix("Validate").WithField("commandName", c.Name) validateLogger.Debug("Validating command") - if c.Regex == "" && len(c.Regexes) == 0 { - validateLogger.Error("Validation failed: Regex pattern is required") - return fmt.Errorf("pattern is required") + + // For JSON mode, regex patterns are not required + if !c.JSON { + if c.Regex == "" && len(c.Regexes) == 0 { + validateLogger.Error("Validation failed: Regex pattern is required for non-JSON mode") + return fmt.Errorf("pattern is required for non-JSON mode") + } } + if c.Lua == "" { validateLogger.Error("Validation failed: Lua expression is required") return fmt.Errorf("lua expression is required")