package lua import ( "fmt" "strings" "time" golua "github.com/aarzilli/golua/lua" ) // httpArgIdx is the stack slot holding the caller's table. Verified against // golua: a PushGoClosure function is invoked with its arguments already on top // and nothing of itself below them. const httpArgIdx = 1 // readHTTPRequestSpec decodes the Lua argument table: // // __llmsproxy_http({ // url = "https://host/path", // method = "GET", -- optional, default GET // headers = { ["X-A"] = "1" }, -- optional // body = "...", -- optional // timeout_ms = 8000, -- optional, clamped to 60s // }) // // The table is at index 1: measured, not assumed. A Go closure registered with // PushGoClosure is NOT on the stack when it runs — GetTop() is 1 and slot 1 is // the caller's table (type LUA_TTABLE). An earlier version assumed the closure // occupied slot 1 and read from slot 2, which then reported "expected a table // argument" for a perfectly good call. // // luaTableToGoValue already turns the table into a map/slice, so the whole // argument is decoded in one pass instead of walking the table field by field. func readHTTPRequestSpec(L *golua.State) (httpRequestSpec, error) { var spec httpRequestSpec if L.GetTop() < httpArgIdx { return spec, fmt.Errorf("__llmsproxy_http: expected a table argument") } if L.Type(httpArgIdx) != golua.LUA_TTABLE { return spec, fmt.Errorf("__llmsproxy_http: expected a table, got %s", normalizeString(L, httpArgIdx)) } arg, ok := luaTableToGoValue(L, httpArgIdx).(map[string]interface{}) if !ok { return spec, fmt.Errorf("__llmsproxy_http: argument must be a keyed table") } for k, v := range arg { switch strings.ToLower(k) { case "url", "uri", "endpoint": spec.URL = asString(v) case "method", "verb": spec.Method = asString(v) case "body", "data", "payload": spec.Body = asString(v) case "headers", "header": if m, ok := v.(map[string]interface{}); ok { spec.Headers = make(map[string]string, len(m)) for hk, hv := range m { spec.Headers[hk] = asString(hv) } } case "timeout_ms", "timeoutms": if n, ok := asFloat(v); ok { spec.Timeout = time.Duration(n) * time.Millisecond } } } if spec.URL == "" { return spec, fmt.Errorf("__llmsproxy_http: missing url") } return spec, nil } // pushHTTPResponse converts the result into two Lua values: a table // {status, headers, body} and nil. The caller pushes the error slot itself. func pushHTTPResponse(L *golua.State, r *httpResult) { L.NewTable() L.PushString("status") L.PushNumber(float64(r.Status)) L.SetTable(-3) L.PushString("headers") pushStringMap(L, r.Headers) L.SetTable(-3) L.PushString("body") L.PushString(r.Body) L.SetTable(-3) } func pushStringMap(L *golua.State, m map[string]string) { L.NewTable() for k, v := range m { L.PushString(v) L.SetField(-2, k) } } func asString(v interface{}) string { switch t := v.(type) { case string: return t case nil: return "" default: return fmt.Sprintf("%v", t) } } func asFloat(v interface{}) (float64, bool) { switch t := v.(type) { case float64: return t, true case int: return float64(t), true case int64: return float64(t), true default: return 0, false } }