package processor import ( _ "embed" "fmt" "io" "net/http" "regexp" "strings" "cook/utils" logger "git.site.quack-lab.dev/dave/cylogger" lua "github.com/yuin/gopher-lua" ) //go:embed luahelper.lua var helperScript string // processorLogger is a scoped logger for the processor package. var processorLogger = logger.Default.WithPrefix("processor") // Maybe we make this an interface again for the shits and giggles // We will see, it could easily be... var globalVariables = map[string]interface{}{} func SetVariables(vars map[string]interface{}) { for k, v := range vars { globalVariables[k] = v } } func NewLuaState() (*lua.LState, error) { newLStateLogger := processorLogger.WithPrefix("NewLuaState") newLStateLogger.Debug("Creating new Lua state") L := lua.NewState() // defer L.Close() // Load math library newLStateLogger.Debug("Loading Lua math library") L.Push(L.GetGlobal("require")) L.Push(lua.LString("math")) if err := L.PCall(1, 1, nil); err != nil { newLStateLogger.Error("Failed to load Lua math library: %v", err) return nil, fmt.Errorf("error loading Lua math library: %v", err) } newLStateLogger.Debug("Lua math library loaded") // Initialize helper functions newLStateLogger.Debug("Initializing Lua helper functions") if err := InitLuaHelpers(L); err != nil { newLStateLogger.Error("Failed to initialize Lua helper functions: %v", err) return nil, err } newLStateLogger.Debug("Lua helper functions initialized") // Inject global variables if len(globalVariables) > 0 { newLStateLogger.Debug("Injecting %d global variables into Lua state", len(globalVariables)) for k, v := range globalVariables { switch val := v.(type) { case int: L.SetGlobal(k, lua.LNumber(float64(val))) case int64: L.SetGlobal(k, lua.LNumber(float64(val))) case float32: L.SetGlobal(k, lua.LNumber(float64(val))) case float64: L.SetGlobal(k, lua.LNumber(val)) case string: L.SetGlobal(k, lua.LString(val)) case bool: if val { L.SetGlobal(k, lua.LTrue) } else { L.SetGlobal(k, lua.LFalse) } default: // Fallback to string representation L.SetGlobal(k, lua.LString(fmt.Sprintf("%v", val))) } } } newLStateLogger.Debug("New Lua state created successfully") return L, nil } // FromLua converts a Lua table to a struct or map recursively func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) { fromLuaLogger := processorLogger.WithPrefix("FromLua").WithField("luaType", luaValue.Type().String()) fromLuaLogger.Debug("Converting Lua value to Go interface") switch v := luaValue.(type) { case *lua.LTable: fromLuaLogger.Debug("Processing Lua table") isArray, err := IsLuaTableArray(L, v) if err != nil { fromLuaLogger.Error("Failed to determine if Lua table is array: %v", err) return nil, err } fromLuaLogger.Debug("Lua table is array: %t", isArray) if isArray { fromLuaLogger.Debug("Converting Lua table to Go array") result := make([]interface{}, 0) v.ForEach(func(key lua.LValue, value lua.LValue) { converted, _ := FromLua(L, value) result = append(result, converted) }) fromLuaLogger.Trace("Converted Go array: %v", result) return result, nil } else { fromLuaLogger.Debug("Converting Lua table to Go map") result := make(map[string]interface{}) v.ForEach(func(key lua.LValue, value lua.LValue) { converted, _ := FromLua(L, value) result[key.String()] = converted }) fromLuaLogger.Trace("Converted Go map: %v", result) return result, nil } case lua.LString: fromLuaLogger.Debug("Converting Lua string to Go string") fromLuaLogger.Trace("Lua string: %q", string(v)) return string(v), nil case lua.LBool: fromLuaLogger.Debug("Converting Lua boolean to Go boolean") fromLuaLogger.Trace("Lua boolean: %t", bool(v)) return bool(v), nil case lua.LNumber: fromLuaLogger.Debug("Converting Lua number to Go float64") fromLuaLogger.Trace("Lua number: %f", float64(v)) return float64(v), nil default: fromLuaLogger.Debug("Unsupported Lua type, returning nil") return nil, nil } } func IsLuaTableArray(L *lua.LState, v *lua.LTable) (bool, error) { isLuaTableArrayLogger := processorLogger.WithPrefix("IsLuaTableArray") isLuaTableArrayLogger.Debug("Checking if Lua table is an array") isLuaTableArrayLogger.Trace("Lua table input: %v", v) L.SetGlobal("table_to_check", v) // Use our predefined helper function from InitLuaHelpers err := L.DoString(`is_array = isArray(table_to_check)`) if err != nil { isLuaTableArrayLogger.Error("Error determining if table is an array: %v", err) return false, fmt.Errorf("error determining if table is array: %w", err) } // Check the result of our Lua function isArray := L.GetGlobal("is_array") // LVIsFalse returns true if a given LValue is a nil or false otherwise false. result := !lua.LVIsFalse(isArray) isLuaTableArrayLogger.Debug("Lua table is array: %t", result) isLuaTableArrayLogger.Trace("isArray result Lua value: %v", isArray) return result, nil } // InitLuaHelpers initializes common Lua helper functions func InitLuaHelpers(L *lua.LState) error { initLuaHelpersLogger := processorLogger.WithPrefix("InitLuaHelpers") initLuaHelpersLogger.Debug("Loading Lua helper functions") if err := L.DoString(helperScript); err != nil { initLuaHelpersLogger.Error("Failed to load Lua helper functions: %v", err) return fmt.Errorf("error loading helper functions: %v", err) } initLuaHelpersLogger.Debug("Lua helper functions loaded") initLuaHelpersLogger.Debug("Setting up Lua print function to Go") L.SetGlobal("print", L.NewFunction(printToGo)) L.SetGlobal("fetch", L.NewFunction(fetch)) L.SetGlobal("re", L.NewFunction(EvalRegex)) initLuaHelpersLogger.Debug("Lua print and fetch functions bound to Go") return nil } func PrependLuaAssignment(luaExpr string) string { prependLuaAssignmentLogger := processorLogger.WithPrefix("PrependLuaAssignment").WithField("originalLuaExpr", luaExpr) prependLuaAssignmentLogger.Debug("Prepending Lua assignment if necessary") // Auto-prepend v1 for expressions starting with operators if strings.HasPrefix(luaExpr, "*") || strings.HasPrefix(luaExpr, "/") || strings.HasPrefix(luaExpr, "+") || strings.HasPrefix(luaExpr, "-") || strings.HasPrefix(luaExpr, "^") || strings.HasPrefix(luaExpr, "%") { luaExpr = "v1 = v1" + luaExpr prependLuaAssignmentLogger.Debug("Prepended 'v1 = v1' due to operator prefix") } else if strings.HasPrefix(luaExpr, "=") { // Handle direct assignment with = operator luaExpr = "v1 " + luaExpr prependLuaAssignmentLogger.Debug("Prepended 'v1' due to direct assignment operator") } // Add assignment if needed if !strings.Contains(luaExpr, "=") { luaExpr = "v1 = " + luaExpr prependLuaAssignmentLogger.Debug("Prepended 'v1 =' as no assignment was found") } prependLuaAssignmentLogger.Trace("Final Lua expression after prepending: %q", luaExpr) return luaExpr } // BuildLuaScript prepares a Lua expression from shorthand notation func BuildLuaScript(luaExpr string) string { buildLuaScriptLogger := processorLogger.WithPrefix("BuildLuaScript").WithField("inputLuaExpr", luaExpr) buildLuaScriptLogger.Debug("Building full Lua script from expression") // Perform $var substitutions from globalVariables luaExpr = replaceVariables(luaExpr) luaExpr = PrependLuaAssignment(luaExpr) fullScript := fmt.Sprintf(` function run() %s end local res = run() modified = res == nil or res `, luaExpr) buildLuaScriptLogger.Trace("Generated full Lua script: %q", utils.LimitString(fullScript, 200)) 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+)`) return varNameRe.ReplaceAllStringFunc(expr, func(m string) string { name := varNameRe.FindStringSubmatch(m)[1] if v, ok := globalVariables[name]; ok { switch val := v.(type) { case int, int64, float32, float64: return fmt.Sprintf("%v", val) case bool: if val { return "true" } else { return "false" } case string: // Quote strings for Lua literal return fmt.Sprintf("%q", val) default: return fmt.Sprintf("%q", fmt.Sprintf("%v", val)) } } return m }) } func printToGo(L *lua.LState) int { printToGoLogger := processorLogger.WithPrefix("printToGo") printToGoLogger.Debug("Lua print function called, redirecting to Go logger") top := L.GetTop() args := make([]interface{}, top) for i := 1; i <= top; i++ { args[i-1] = L.Get(i) } // Format the message with proper spacing between arguments var parts []string for _, arg := range args { parts = append(parts, fmt.Sprintf("%v", arg)) } message := strings.Join(parts, " ") printToGoLogger.Trace("Lua print message: %q", message) // Use the LUA log level with a script tag logger.Lua("%s", message) printToGoLogger.Debug("Message logged from Lua") return 0 } func fetch(L *lua.LState) int { fetchLogger := processorLogger.WithPrefix("fetch") fetchLogger.Debug("Lua fetch function called") // Get URL from first argument url := L.ToString(1) if url == "" { fetchLogger.Error("Fetch failed: URL is required") L.Push(lua.LNil) L.Push(lua.LString("URL is required")) return 2 } fetchLogger.Debug("Fetching URL: %q", url) // Get options from second argument if provided var method = "GET" var headers = make(map[string]string) var body = "" if L.GetTop() > 1 { options := L.ToTable(2) if options != nil { fetchLogger.Debug("Processing fetch options") // Get method if methodVal := options.RawGetString("method"); methodVal != lua.LNil { method = methodVal.String() fetchLogger.Trace("Method from options: %q", method) } // Get headers if headersVal := options.RawGetString("headers"); headersVal != lua.LNil { if headersTable, ok := headersVal.(*lua.LTable); ok { fetchLogger.Trace("Processing headers table") headersTable.ForEach(func(key lua.LValue, value lua.LValue) { headers[key.String()] = value.String() fetchLogger.Trace("Header: %q = %q", key.String(), value.String()) }) } fetchLogger.Trace("All headers: %v", headers) } // Get body if bodyVal := options.RawGetString("body"); bodyVal != lua.LNil { body = bodyVal.String() fetchLogger.Trace("Body from options: %q", utils.LimitString(body, 100)) } } } fetchLogger.Debug("Fetch request details: Method=%q, URL=%q, BodyLength=%d, Headers=%v", method, url, len(body), headers) // Create HTTP request req, err := http.NewRequest(method, url, strings.NewReader(body)) if err != nil { fetchLogger.Error("Error creating HTTP request: %v", err) L.Push(lua.LNil) L.Push(lua.LString(fmt.Sprintf("Error creating request: %v", err))) return 2 } // Set headers for key, value := range headers { req.Header.Set(key, value) } fetchLogger.Debug("HTTP request created and headers set") fetchLogger.Trace("HTTP Request: %+v", req) // Make request client := &http.Client{} resp, err := client.Do(req) if err != nil { fetchLogger.Error("Error making HTTP request: %v", err) L.Push(lua.LNil) L.Push(lua.LString(fmt.Sprintf("Error making request: %v", err))) return 2 } defer func() { fetchLogger.Debug("Closing HTTP response body") resp.Body.Close() }() fetchLogger.Debug("HTTP request executed. Status Code: %d", resp.StatusCode) // Read response body bodyBytes, err := io.ReadAll(resp.Body) if err != nil { fetchLogger.Error("Error reading response body: %v", err) L.Push(lua.LNil) L.Push(lua.LString(fmt.Sprintf("Error reading response: %v", err))) return 2 } fetchLogger.Trace("Response body length: %d", len(bodyBytes)) // Create response table responseTable := L.NewTable() responseTable.RawSetString("status", lua.LNumber(resp.StatusCode)) responseTable.RawSetString("statusText", lua.LString(resp.Status)) responseTable.RawSetString("ok", lua.LBool(resp.StatusCode >= 200 && resp.StatusCode < 300)) responseTable.RawSetString("body", lua.LString(string(bodyBytes))) fetchLogger.Debug("Created Lua response table") // Set headers in response headersTable := L.NewTable() for key, values := range resp.Header { headersTable.RawSetString(key, lua.LString(values[0])) fetchLogger.Trace("Response header: %q = %q", key, values[0]) } responseTable.RawSetString("headers", headersTable) fetchLogger.Trace("Full response table: %v", responseTable) L.Push(responseTable) fetchLogger.Debug("Pushed response table to Lua stack") return 1 } func EvalRegex(L *lua.LState) int { evalRegexLogger := processorLogger.WithPrefix("evalRegex") evalRegexLogger.Debug("Lua evalRegex function called") pattern := L.ToString(1) input := L.ToString(2) evalRegexLogger.Debug("Pattern: %q, Input: %q", pattern, input) re := regexp.MustCompile(pattern) matches := re.FindStringSubmatch(input) evalRegexLogger.Debug("Go regex matches: %v (count: %d)", matches, len(matches)) evalRegexLogger.Debug("Matches is nil: %t", matches == nil) if len(matches) > 0 { matchesTable := L.NewTable() for i, match := range matches { matchesTable.RawSetInt(i+1, lua.LString(match)) evalRegexLogger.Debug("Set table[%d] = %q", i+1, match) } L.Push(matchesTable) } else { L.Push(lua.LNil) } evalRegexLogger.Debug("Pushed matches table to Lua stack") return 1 } // GetLuaFunctionsHelp returns a comprehensive help string for all available Lua functions func GetLuaFunctionsHelp() string { return `Lua Functions Available in Global Environment: MATH FUNCTIONS: min(a, b) - Returns the minimum of two numbers max(a, b) - Returns the maximum of two numbers round(x, n) - Rounds x to n decimal places (default 0) floor(x) - Returns the floor of x ceil(x) - Returns the ceiling of x STRING FUNCTIONS: upper(s) - Converts string to uppercase lower(s) - Converts string to lowercase format(s, ...) - Formats string using Lua string.format trim(s) - Removes leading/trailing whitespace strsplit(inputstr, sep) - Splits string by separator (default: whitespace) fromCSV(csv, delimiter, hasHeaders) - Parses CSV text into rows of fields (delimiter defaults to ",", hasHeaders defaults to false) toCSV(rows, delimiter) - Converts table of rows to CSV text format (delimiter defaults to ",") num(str) - Converts string to number (returns 0 if invalid) str(num) - Converts number to string is_number(str) - Returns true if string is numeric TABLE FUNCTIONS: dump(table, depth) - Prints table structure recursively isArray(t) - Returns true if table is a sequential array HTTP FUNCTIONS: fetch(url, options) - Makes HTTP request, returns response table options: {method="GET", headers={}, body=""} returns: {status, statusText, ok, body, headers} REGEX FUNCTIONS: re(pattern, input) - Applies regex pattern to input string returns: table with matches (index 0 = full match, 1+ = groups) UTILITY FUNCTIONS: print(...) - Prints arguments to Go logger EXAMPLES: round(3.14159, 2) -> 3.14 strsplit("a,b,c", ",") -> {"a", "b", "c"} upper("hello") -> "HELLO" min(5, 3) -> 3 num("123") -> 123 is_number("abc") -> false fetch("https://api.example.com/data") re("(\\w+)@(\\w+)", "user@domain.com") -> {"user@domain.com", "user", "domain.com"}` }