568 lines
17 KiB
Go
568 lines
17 KiB
Go
package processor
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"cook/utils"
|
|
|
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
|
lua "github.com/yuin/gopher-lua"
|
|
)
|
|
|
|
// 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")
|
|
|
|
helperScript := `
|
|
-- Custom Lua helpers for math operations
|
|
function min(a, b) return math.min(a, b) end
|
|
function max(a, b) return math.max(a, b) end
|
|
function round(x, n)
|
|
if n == nil then n = 0 end
|
|
return math.floor(x * 10^n + 0.5) / 10^n
|
|
end
|
|
function floor(x) return math.floor(x) end
|
|
function ceil(x) return math.ceil(x) end
|
|
function upper(s) return string.upper(s) end
|
|
function lower(s) return string.lower(s) end
|
|
function format(s, ...) return string.format(s, ...) end
|
|
function trim(s) return string.gsub(s, "^%s*(.-)%s*$", "%1") end
|
|
|
|
-- String split helper
|
|
function strsplit(inputstr, sep)
|
|
if sep == nil then
|
|
sep = "%s"
|
|
end
|
|
local t = {}
|
|
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
|
|
table.insert(t, str)
|
|
end
|
|
return t
|
|
end
|
|
|
|
---@param table table
|
|
---@param depth number?
|
|
function DumpTable(table, depth)
|
|
if depth == nil then
|
|
depth = 0
|
|
end
|
|
if (depth > 200) then
|
|
print("Error: Depth > 200 in dumpTable()")
|
|
return
|
|
end
|
|
for k, v in pairs(table) do
|
|
if (type(v) == "table") then
|
|
print(string.rep(" ", depth) .. k .. ":")
|
|
DumpTable(v, depth + 1)
|
|
else
|
|
print(string.rep(" ", depth) .. k .. ": ", v)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- String to number conversion helper
|
|
function num(str)
|
|
return tonumber(str) or 0
|
|
end
|
|
|
|
-- Number to string conversion
|
|
function str(num)
|
|
return tostring(num)
|
|
end
|
|
|
|
-- Check if string is numeric
|
|
function is_number(str)
|
|
return tonumber(str) ~= nil
|
|
end
|
|
|
|
function isArray(t)
|
|
if type(t) ~= "table" then return false end
|
|
local max = 0
|
|
local count = 0
|
|
for k, _ in pairs(t) do
|
|
if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then
|
|
return false
|
|
end
|
|
max = math.max(max, k)
|
|
count = count + 1
|
|
end
|
|
return max == count
|
|
end
|
|
|
|
modified = false
|
|
`
|
|
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 string = "GET"
|
|
var headers map[string]string = make(map[string]string)
|
|
var body string = ""
|
|
|
|
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")
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
evalRegexLogger.Error("Panic in EvalRegex: %v", r)
|
|
// Push empty table on panic
|
|
emptyTable := L.NewTable()
|
|
L.Push(emptyTable)
|
|
}
|
|
}()
|
|
|
|
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))
|
|
|
|
matchesTable := L.NewTable()
|
|
for i, match := range matches {
|
|
matchesTable.RawSetInt(i, lua.LString(match))
|
|
evalRegexLogger.Debug("Set table[%d] = %q", i, match)
|
|
}
|
|
|
|
L.Push(matchesTable)
|
|
evalRegexLogger.Debug("Pushed matches table to Lua stack")
|
|
|
|
return 1
|
|
}
|
|
|
|
// GetLuaFunctionsHelp returns a comprehensive help string for all available Lua functions
|
|
func GetLuaFunctionsHelp() string {
|
|
return `Lua Functions Available in Global Environment:
|
|
|
|
MATH FUNCTIONS:
|
|
min(a, b) - Returns the minimum of two numbers
|
|
max(a, b) - Returns the maximum of two numbers
|
|
round(x, n) - Rounds x to n decimal places (default 0)
|
|
floor(x) - Returns the floor of x
|
|
ceil(x) - Returns the ceiling of x
|
|
|
|
STRING FUNCTIONS:
|
|
upper(s) - Converts string to uppercase
|
|
lower(s) - Converts string to lowercase
|
|
format(s, ...) - Formats string using Lua string.format
|
|
trim(s) - Removes leading/trailing whitespace
|
|
strsplit(inputstr, sep) - Splits string by separator (default: whitespace)
|
|
num(str) - Converts string to number (returns 0 if invalid)
|
|
str(num) - Converts number to string
|
|
is_number(str) - Returns true if string is numeric
|
|
|
|
TABLE FUNCTIONS:
|
|
DumpTable(table, depth) - Prints table structure recursively
|
|
isArray(t) - Returns true if table is a sequential array
|
|
|
|
HTTP FUNCTIONS:
|
|
fetch(url, options) - Makes HTTP request, returns response table
|
|
options: {method="GET", headers={}, body=""}
|
|
returns: {status, statusText, ok, body, headers}
|
|
|
|
REGEX FUNCTIONS:
|
|
re(pattern, input) - Applies regex pattern to input string
|
|
returns: table with matches (index 0 = full match, 1+ = groups)
|
|
|
|
UTILITY FUNCTIONS:
|
|
print(...) - Prints arguments to Go logger
|
|
|
|
EXAMPLES:
|
|
round(3.14159, 2) -> 3.14
|
|
strsplit("a,b,c", ",") -> {"a", "b", "c"}
|
|
upper("hello") -> "HELLO"
|
|
min(5, 3) -> 3
|
|
num("123") -> 123
|
|
is_number("abc") -> false
|
|
fetch("https://api.example.com/data")
|
|
re("(\\w+)@(\\w+)", "user@domain.com") -> {"user@domain.com", "user", "domain.com"}`
|
|
}
|