add scripting js/lua

This commit is contained in:
2026-01-28 14:21:08 +04:00
parent c3ea3a34ad
commit 533ced4b54
13 changed files with 968 additions and 27 deletions

191
internal/script/lua.go Normal file
View File

@@ -0,0 +1,191 @@
package script
import (
"errors"
"fmt"
lua "github.com/yuin/gopher-lua"
)
// LuaEngine implements the Engine interface using gopher-lua.
type LuaEngine struct {
state *lua.LState
transform *lua.LFunction
}
// NewLuaEngine creates a new Lua script engine with the given script content.
// The script must define a global `transform` function that takes a request table
// and returns the modified request table.
//
// Example Lua script:
//
// function transform(req)
// req.headers["X-Custom"] = "value"
// return req
// end
func NewLuaEngine(scriptContent string) (*LuaEngine, error) {
L := lua.NewState()
// Execute the script to define the transform function
if err := L.DoString(scriptContent); err != nil {
L.Close()
return nil, fmt.Errorf("failed to execute Lua script: %w", err)
}
// Get the transform function
transform := L.GetGlobal("transform")
if transform.Type() != lua.LTFunction {
L.Close()
return nil, errors.New("script must define a global 'transform' function")
}
return &LuaEngine{
state: L,
transform: transform.(*lua.LFunction),
}, nil
}
// Transform executes the Lua transform function with the given request data.
func (e *LuaEngine) Transform(req *RequestData) error {
// Convert RequestData to Lua table
reqTable := e.requestDataToTable(req)
// Call transform(req)
e.state.Push(e.transform)
e.state.Push(reqTable)
if err := e.state.PCall(1, 1, nil); err != nil {
return fmt.Errorf("lua transform error: %w", err)
}
// Get the result
result := e.state.Get(-1)
e.state.Pop(1)
if result.Type() != lua.LTTable {
return fmt.Errorf("transform function must return a table, got %s", result.Type())
}
// Update RequestData from the returned table
e.tableToRequestData(result.(*lua.LTable), req)
return nil
}
// Close releases the Lua state resources.
func (e *LuaEngine) Close() {
if e.state != nil {
e.state.Close()
}
}
// requestDataToTable converts RequestData to a Lua table.
func (e *LuaEngine) requestDataToTable(req *RequestData) *lua.LTable {
L := e.state
t := L.NewTable()
t.RawSetString("method", lua.LString(req.Method))
t.RawSetString("url", lua.LString(req.URL))
t.RawSetString("path", lua.LString(req.Path))
t.RawSetString("body", lua.LString(req.Body))
// Headers (map[string][]string -> table of arrays)
headers := L.NewTable()
for k, values := range req.Headers {
arr := L.NewTable()
for _, v := range values {
arr.Append(lua.LString(v))
}
headers.RawSetString(k, arr)
}
t.RawSetString("headers", headers)
// Params (map[string][]string -> table of arrays)
params := L.NewTable()
for k, values := range req.Params {
arr := L.NewTable()
for _, v := range values {
arr.Append(lua.LString(v))
}
params.RawSetString(k, arr)
}
t.RawSetString("params", params)
// Cookies (map[string][]string -> table of arrays)
cookies := L.NewTable()
for k, values := range req.Cookies {
arr := L.NewTable()
for _, v := range values {
arr.Append(lua.LString(v))
}
cookies.RawSetString(k, arr)
}
t.RawSetString("cookies", cookies)
return t
}
// tableToRequestData updates RequestData from a Lua table.
func (e *LuaEngine) tableToRequestData(t *lua.LTable, req *RequestData) {
// Method
if v := t.RawGetString("method"); v.Type() == lua.LTString {
req.Method = string(v.(lua.LString))
}
// URL
if v := t.RawGetString("url"); v.Type() == lua.LTString {
req.URL = string(v.(lua.LString))
}
// Path
if v := t.RawGetString("path"); v.Type() == lua.LTString {
req.Path = string(v.(lua.LString))
}
// Body
if v := t.RawGetString("body"); v.Type() == lua.LTString {
req.Body = string(v.(lua.LString))
}
// Headers
if v := t.RawGetString("headers"); v.Type() == lua.LTTable {
req.Headers = e.tableToStringSliceMap(v.(*lua.LTable))
}
// Params
if v := t.RawGetString("params"); v.Type() == lua.LTTable {
req.Params = e.tableToStringSliceMap(v.(*lua.LTable))
}
// Cookies
if v := t.RawGetString("cookies"); v.Type() == lua.LTTable {
req.Cookies = e.tableToStringSliceMap(v.(*lua.LTable))
}
}
// tableToStringSliceMap converts a Lua table to a Go map[string][]string.
// Supports both single string values and array values.
func (e *LuaEngine) tableToStringSliceMap(t *lua.LTable) map[string][]string {
result := make(map[string][]string)
t.ForEach(func(k, v lua.LValue) {
if k.Type() != lua.LTString {
return
}
key := string(k.(lua.LString))
switch v.Type() {
case lua.LTString:
// Single string value
result[key] = []string{string(v.(lua.LString))}
case lua.LTTable:
// Array of strings
var values []string
v.(*lua.LTable).ForEach(func(_, item lua.LValue) {
if item.Type() == lua.LTString {
values = append(values, string(item.(lua.LString)))
}
})
result[key] = values
}
})
return result
}