672 lines
17 KiB
Go
672 lines
17 KiB
Go
package plugins
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
lua "github.com/yuin/gopher-lua"
|
|
|
|
// worklog import is only used in the registerWorklogAPI closure
|
|
// where the compiler resolves it via the import statement.
|
|
"verstak/internal/core/worklog"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// verstak.db.* — plugin's own SQLite database
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// pluginDBMu guards the pluginDB cache.
|
|
var pluginDBMu sync.Mutex
|
|
var pluginDBs = make(map[string]*sql.DB)
|
|
|
|
// openPluginDB opens (or returns cached) the plugin's private SQLite DB.
|
|
func openPluginDB(vm *LuaVM) (*sql.DB, error) {
|
|
name := vm.Plugin.Meta.Name
|
|
pluginDBMu.Lock()
|
|
defer pluginDBMu.Unlock()
|
|
|
|
if db, ok := pluginDBs[name]; ok {
|
|
return db, nil
|
|
}
|
|
dbPath := filepath.Join(vm.Plugin.DataDir, "data.db")
|
|
os.MkdirAll(vm.Plugin.DataDir, 0o750)
|
|
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=rwc&_journal_mode=WAL", dbPath))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open plugin db: %w", err)
|
|
}
|
|
pluginDBs[name] = db
|
|
return db, nil
|
|
}
|
|
|
|
// closePluginDB closes a plugin's DB.
|
|
func closePluginDB(name string) {
|
|
pluginDBMu.Lock()
|
|
defer pluginDBMu.Unlock()
|
|
if db, ok := pluginDBs[name]; ok {
|
|
db.Close()
|
|
delete(pluginDBs, name)
|
|
}
|
|
}
|
|
|
|
func registerDBAPI(vm *LuaVM) *lua.LTable {
|
|
L := vm.L
|
|
tbl := L.NewTable()
|
|
|
|
tbl.RawSetString("exec", L.NewFunction(func(L *lua.LState) int {
|
|
query := L.CheckString(1)
|
|
args := collectArgs(L, 2)
|
|
db, err := openPluginDB(vm)
|
|
if err != nil {
|
|
return pushError(L, err)
|
|
}
|
|
_, err = db.Exec(query, args...)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("exec: %w", err))
|
|
}
|
|
L.Push(lua.LBool(true))
|
|
return 1
|
|
}))
|
|
|
|
tbl.RawSetString("query", L.NewFunction(func(L *lua.LState) int {
|
|
query := L.CheckString(1)
|
|
args := collectArgs(L, 2)
|
|
db, err := openPluginDB(vm)
|
|
if err != nil {
|
|
return pushError(L, err)
|
|
}
|
|
rows, err := db.Query(query, args...)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("query: %w", err))
|
|
}
|
|
defer rows.Close()
|
|
|
|
cols, _ := rows.Columns()
|
|
arr := L.NewTable()
|
|
rowIdx := 1
|
|
for rows.Next() {
|
|
vals := make([]interface{}, len(cols))
|
|
ptrs := make([]interface{}, len(cols))
|
|
for i := range vals {
|
|
ptrs[i] = &vals[i]
|
|
}
|
|
if err := rows.Scan(ptrs...); err != nil {
|
|
return pushError(L, fmt.Errorf("scan: %w", err))
|
|
}
|
|
row := L.NewTable()
|
|
for i, col := range cols {
|
|
row.RawSetString(col, goValueToLua(L, vals[i]))
|
|
}
|
|
arr.RawSetInt(rowIdx, row)
|
|
rowIdx++
|
|
}
|
|
L.Push(arr)
|
|
return 1
|
|
}))
|
|
|
|
tbl.RawSetString("query_row", L.NewFunction(func(L *lua.LState) int {
|
|
query := L.CheckString(1)
|
|
args := collectArgs(L, 2)
|
|
db, err := openPluginDB(vm)
|
|
if err != nil {
|
|
return pushError(L, err)
|
|
}
|
|
rows, err := db.Query(query, args...)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("query_row: %w", err))
|
|
}
|
|
defer rows.Close()
|
|
|
|
if !rows.Next() {
|
|
L.Push(lua.LNil)
|
|
return 1
|
|
}
|
|
|
|
cols, _ := rows.Columns()
|
|
vals := make([]interface{}, len(cols))
|
|
ptrs := make([]interface{}, len(cols))
|
|
for i := range vals {
|
|
ptrs[i] = &vals[i]
|
|
}
|
|
if err := rows.Scan(ptrs...); err != nil {
|
|
return pushError(L, fmt.Errorf("query_row: %w", err))
|
|
}
|
|
tbl := L.NewTable()
|
|
for i, col := range cols {
|
|
tbl.RawSetString(col, goValueToLua(L, vals[i]))
|
|
}
|
|
L.Push(tbl)
|
|
return 1
|
|
}))
|
|
|
|
return tbl
|
|
}
|
|
|
|
// collectArgs extracts SQL arguments from Lua varargs starting at pos.
|
|
func collectArgs(L *lua.LState, pos int) []interface{} {
|
|
var args []interface{}
|
|
top := L.GetTop()
|
|
for i := pos; i <= top; i++ {
|
|
val := L.Get(i)
|
|
args = append(args, luaValueToGo(val))
|
|
}
|
|
return args
|
|
}
|
|
|
|
// guessColumns returns placeholder column names for query_row.
|
|
func guessColumns(query string) []string {
|
|
upper := strings.ToUpper(strings.TrimSpace(query))
|
|
if strings.HasPrefix(upper, "SELECT COUNT") || strings.Contains(upper, "COUNT(") {
|
|
// Single-result aggregate
|
|
if strings.Contains(upper, "COUNT(DISTINCT") {
|
|
return []string{"count"}
|
|
}
|
|
return []string{"count"}
|
|
}
|
|
// Default — caller can use array access
|
|
return []string{"col1", "col2", "col3", "col4", "col5", "col6", "col7", "col8"}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// verstak.config.* — JSON config per plugin
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func loadPluginConfig(vm *LuaVM) (map[string]interface{}, error) {
|
|
cfgPath := filepath.Join(vm.Plugin.DataDir, "config.json")
|
|
b, err := os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return make(map[string]interface{}), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(b, &m); err != nil {
|
|
return nil, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func savePluginConfig(vm *LuaVM, m map[string]interface{}) error {
|
|
os.MkdirAll(vm.Plugin.DataDir, 0o750)
|
|
b, err := json.MarshalIndent(m, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfgPath := filepath.Join(vm.Plugin.DataDir, "config.json")
|
|
return os.WriteFile(cfgPath, b, 0o640)
|
|
}
|
|
|
|
func registerConfigAPI(vm *LuaVM) *lua.LTable {
|
|
L := vm.L
|
|
tbl := L.NewTable()
|
|
|
|
// state is a map[string]interface{} (in memory, written to disk on set).
|
|
cfg, _ := loadPluginConfig(vm)
|
|
|
|
tbl.RawSetString("get", L.NewFunction(func(L *lua.LState) int {
|
|
key := L.CheckString(1)
|
|
val, ok := cfg[key]
|
|
if !ok {
|
|
L.Push(lua.LNil)
|
|
return 1
|
|
}
|
|
L.Push(toLuaValue(L, val))
|
|
return 1
|
|
}))
|
|
|
|
tbl.RawSetString("set", L.NewFunction(func(L *lua.LState) int {
|
|
key := L.CheckString(1)
|
|
val := luaValueToGo(L.Get(2))
|
|
cfg[key] = val
|
|
if err := savePluginConfig(vm, cfg); err != nil {
|
|
return pushError(L, fmt.Errorf("config.save: %w", err))
|
|
}
|
|
L.Push(lua.LBool(true))
|
|
return 1
|
|
}))
|
|
|
|
tbl.RawSetString("all", L.NewFunction(func(L *lua.LState) int {
|
|
L.Push(luaTableFromMap(L, cfg))
|
|
return 1
|
|
}))
|
|
|
|
return tbl
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// verstak.state.* — already implemented (in-memory map per plugin)
|
|
// Still present in api_ext.go — see registerStateAPI below.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func registerStateAPI(vm *LuaVM) *lua.LTable {
|
|
L := vm.L
|
|
tbl := L.NewTable()
|
|
|
|
state := make(map[string]interface{})
|
|
|
|
tbl.RawSetString("get", L.NewFunction(func(L *lua.LState) int {
|
|
key := L.CheckString(1)
|
|
val, ok := state[key]
|
|
if !ok {
|
|
L.Push(lua.LNil)
|
|
return 1
|
|
}
|
|
L.Push(toLuaValue(L, val))
|
|
return 1
|
|
}))
|
|
|
|
tbl.RawSetString("set", L.NewFunction(func(L *lua.LState) int {
|
|
key := L.CheckString(1)
|
|
val := L.Get(2)
|
|
state[key] = luaValueToGo(val)
|
|
return 0
|
|
}))
|
|
|
|
tbl.RawSetString("delete", L.NewFunction(func(L *lua.LState) int {
|
|
key := L.CheckString(1)
|
|
delete(state, key)
|
|
return 0
|
|
}))
|
|
|
|
return tbl
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// verstak.ui.* — send events to the frontend
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func registerUIAPI(vm *LuaVM) *lua.LTable {
|
|
L := vm.L
|
|
tbl := L.NewTable()
|
|
|
|
tbl.RawSetString("toast", L.NewFunction(func(L *lua.LState) int {
|
|
msg := L.CheckString(1)
|
|
typ := L.OptString(2, "info")
|
|
// TODO: send event to frontend via Wails events
|
|
log.Printf("[lua] toast(%s): %s", typ, msg)
|
|
return 0
|
|
}))
|
|
|
|
tbl.RawSetString("navigate_to", L.NewFunction(func(L *lua.LState) int {
|
|
page := L.CheckString(1)
|
|
// TODO: send navigate event to frontend
|
|
log.Printf("[lua] navigate_to: %s", page)
|
|
return 0
|
|
}))
|
|
|
|
return tbl
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// verstak.schedule.* — recurring tasks (uses the Scheduler)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func registerScheduleAPI(vm *LuaVM) *lua.LTable {
|
|
L := vm.L
|
|
tbl := L.NewTable()
|
|
|
|
tbl.RawSetString("every", L.NewFunction(func(L *lua.LState) int {
|
|
interval := L.CheckString(1)
|
|
callback := L.CheckString(2)
|
|
|
|
d, err := time.ParseDuration(interval)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("schedule.every: invalid duration %q: %w", interval, err))
|
|
}
|
|
if d < time.Second {
|
|
return pushError(L, fmt.Errorf("schedule.every: minimum interval is 1s"))
|
|
}
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(d)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if err := vm.CallHook(callback); err != nil {
|
|
log.Printf("[lua] schedule %q error: %v", callback, err)
|
|
}
|
|
case <-vm.done:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return 0
|
|
}))
|
|
|
|
return tbl
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// verstak.http.* — simple HTTP requests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func registerHTTPAPI(vm *LuaVM) *lua.LTable {
|
|
L := vm.L
|
|
tbl := L.NewTable()
|
|
|
|
tbl.RawSetString("get", L.NewFunction(func(L *lua.LState) int {
|
|
url := L.CheckString(1)
|
|
headers := L.OptTable(3, L.NewTable())
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("http.get: %w", err))
|
|
}
|
|
applyHeaders(req, headers)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("http.get: %w", err))
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return pushResponse(L, resp)
|
|
}))
|
|
|
|
tbl.RawSetString("post", L.NewFunction(func(L *lua.LState) int {
|
|
url := L.CheckString(1)
|
|
body := L.Get(2)
|
|
headers := L.OptTable(3, L.NewTable())
|
|
|
|
var bodyReader io.Reader
|
|
if body != lua.LNil {
|
|
bodyReader = strings.NewReader(lua.LVAsString(body))
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", url, bodyReader)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("http.post: %w", err))
|
|
}
|
|
if body != lua.LNil {
|
|
req.Header.Set("Content-Type", "application/octet-stream")
|
|
}
|
|
applyHeaders(req, headers)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("http.post: %w", err))
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return pushResponse(L, resp)
|
|
}))
|
|
|
|
return tbl
|
|
}
|
|
|
|
func applyHeaders(req *http.Request, tbl *lua.LTable) {
|
|
tbl.ForEach(func(k, v lua.LValue) {
|
|
key := lua.LVAsString(k)
|
|
val := lua.LVAsString(v)
|
|
if key != "" && val != "" {
|
|
req.Header.Set(key, val)
|
|
}
|
|
})
|
|
}
|
|
|
|
func pushResponse(L *lua.LState, resp *http.Response) int {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
tbl := L.NewTable()
|
|
tbl.RawSetString("status", lua.LNumber(resp.StatusCode))
|
|
tbl.RawSetString("body", lua.LString(string(body)))
|
|
|
|
headers := L.NewTable()
|
|
for k, vals := range resp.Header {
|
|
headers.RawSetString(k, lua.LString(strings.Join(vals, ", ")))
|
|
}
|
|
tbl.RawSetString("headers", headers)
|
|
|
|
L.Push(tbl)
|
|
return 1
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// verstak.worklog.* — real worklog operations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func registerWorklogAPI(vm *LuaVM) *lua.LTable {
|
|
L := vm.L
|
|
tbl := L.NewTable()
|
|
svc := vm.Services
|
|
|
|
// Helper to check if services are available
|
|
checkSvc := func() *worklog.Service {
|
|
if svc == nil || svc.WorklogSvc == nil {
|
|
return nil
|
|
}
|
|
return svc.WorklogSvc
|
|
}
|
|
|
|
tbl.RawSetString("add", L.NewFunction(func(L *lua.LState) int {
|
|
wsvc := checkSvc()
|
|
if wsvc == nil {
|
|
return pushError(L, fmt.Errorf("worklog service not available"))
|
|
}
|
|
nodeID := L.CheckString(1)
|
|
summary := L.CheckString(2)
|
|
minutes := L.CheckInt(3)
|
|
details := L.OptString(4, "")
|
|
approx := lua.LVAsBool(L.Get(5))
|
|
|
|
e, err := wsvc.Add(nodeID, summary, details, minutes, approx, false)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("worklog.add: %w", err))
|
|
}
|
|
tbl := L.NewTable()
|
|
tbl.RawSetString("id", lua.LString(e.ID))
|
|
tbl.RawSetString("node_id", lua.LString(e.NodeID))
|
|
tbl.RawSetString("summary", lua.LString(e.Summary))
|
|
tbl.RawSetString("minutes", lua.LNumber(*e.Minutes))
|
|
tbl.RawSetString("date", lua.LString(e.Date))
|
|
tbl.RawSetString("created_at", lua.LString(e.CreatedAt.Format(time.RFC3339)))
|
|
L.Push(tbl)
|
|
return 1
|
|
}))
|
|
|
|
tbl.RawSetString("list", L.NewFunction(func(L *lua.LState) int {
|
|
wsvc := checkSvc()
|
|
if wsvc == nil {
|
|
return pushError(L, fmt.Errorf("worklog service not available"))
|
|
}
|
|
nodeID := L.CheckString(1)
|
|
entries, err := wsvc.ListByNode(nodeID)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("worklog.list: %w", err))
|
|
}
|
|
arr := L.NewTable()
|
|
for i, e := range entries {
|
|
et := L.NewTable()
|
|
et.RawSetString("id", lua.LString(e.ID))
|
|
et.RawSetString("node_id", lua.LString(e.NodeID))
|
|
et.RawSetString("summary", lua.LString(e.Summary))
|
|
if e.Minutes != nil {
|
|
et.RawSetString("minutes", lua.LNumber(*e.Minutes))
|
|
}
|
|
et.RawSetString("date", lua.LString(e.Date))
|
|
et.RawSetString("created_at", lua.LString(e.CreatedAt.Format(time.RFC3339)))
|
|
arr.RawSetInt(i+1, et)
|
|
}
|
|
L.Push(arr)
|
|
return 1
|
|
}))
|
|
|
|
tbl.RawSetString("summary", L.NewFunction(func(L *lua.LState) int {
|
|
wsvc := checkSvc()
|
|
if wsvc == nil {
|
|
return pushError(L, fmt.Errorf("worklog service not available"))
|
|
}
|
|
nodeID := L.CheckString(1)
|
|
entries, err := wsvc.ListByNode(nodeID)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("worklog.summary: %w", err))
|
|
}
|
|
total := 0
|
|
for _, e := range entries {
|
|
if e.Minutes != nil {
|
|
total += *e.Minutes
|
|
}
|
|
}
|
|
tbl := L.NewTable()
|
|
tbl.RawSetString("total_minutes", lua.LNumber(total))
|
|
tbl.RawSetString("count", lua.LNumber(len(entries)))
|
|
L.Push(tbl)
|
|
return 1
|
|
}))
|
|
|
|
tbl.RawSetString("delete", L.NewFunction(func(L *lua.LState) int {
|
|
wsvc := checkSvc()
|
|
if wsvc == nil {
|
|
return pushError(L, fmt.Errorf("worklog service not available"))
|
|
}
|
|
id := L.CheckString(1)
|
|
if err := wsvc.Delete(id); err != nil {
|
|
return pushError(L, fmt.Errorf("worklog.delete: %w", err))
|
|
}
|
|
L.Push(lua.LBool(true))
|
|
return 1
|
|
}))
|
|
|
|
return tbl
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// verstak.activity.* — real activity feed operations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func registerActivityAPI(vm *LuaVM) *lua.LTable {
|
|
L := vm.L
|
|
tbl := L.NewTable()
|
|
svc := vm.Services
|
|
|
|
tbl.RawSetString("log", L.NewFunction(func(L *lua.LState) int {
|
|
if svc == nil || svc.ActivitySvc == nil {
|
|
return pushError(L, fmt.Errorf("activity service not available"))
|
|
}
|
|
eventType := L.CheckString(1)
|
|
title := L.CheckString(2)
|
|
targetID := L.OptString(3, "")
|
|
nodeID := L.OptString(4, "")
|
|
|
|
err := svc.ActivitySvc.Record(nodeID, "", targetID, "", eventType, title, "")
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("activity.log: %w", err))
|
|
}
|
|
L.Push(lua.LBool(true))
|
|
return 1
|
|
}))
|
|
|
|
tbl.RawSetString("list", L.NewFunction(func(L *lua.LState) int {
|
|
if svc == nil || svc.ActivitySvc == nil {
|
|
return pushError(L, fmt.Errorf("activity service not available"))
|
|
}
|
|
limit := L.OptInt(1, 20)
|
|
events, err := svc.ActivitySvc.ListRecent(limit, 0)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("activity.list: %w", err))
|
|
}
|
|
arr := L.NewTable()
|
|
for i, e := range events {
|
|
et := L.NewTable()
|
|
et.RawSetString("event_type", lua.LString(e.EventType))
|
|
et.RawSetString("title", lua.LString(e.Title))
|
|
et.RawSetString("target_id", lua.LString(e.TargetID))
|
|
et.RawSetString("node_id", lua.LString(e.NodeID))
|
|
et.RawSetString("created_at", lua.LString(e.CreatedAt))
|
|
arr.RawSetInt(i+1, et)
|
|
}
|
|
L.Push(arr)
|
|
return 1
|
|
}))
|
|
|
|
return tbl
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// verstak.file.* — real file operations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func registerFileAPI(vm *LuaVM) *lua.LTable {
|
|
L := vm.L
|
|
tbl := L.NewTable()
|
|
svc := vm.Services
|
|
|
|
tbl.RawSetString("list", L.NewFunction(func(L *lua.LState) int {
|
|
if svc == nil || svc.FilesSvc == nil {
|
|
return pushError(L, fmt.Errorf("file service not available"))
|
|
}
|
|
nodeID := L.CheckString(1)
|
|
records, err := svc.FilesSvc.ListByNode(nodeID)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("file.list: %w", err))
|
|
}
|
|
arr := L.NewTable()
|
|
for i, r := range records {
|
|
ft := L.NewTable()
|
|
ft.RawSetString("id", lua.LString(r.ID))
|
|
ft.RawSetString("filename", lua.LString(r.Filename))
|
|
ft.RawSetString("path", lua.LString(r.Path))
|
|
ft.RawSetString("size", lua.LNumber(r.Size))
|
|
ft.RawSetString("mime", lua.LString(r.MIME))
|
|
ft.RawSetString("missing", lua.LBool(r.Missing))
|
|
ft.RawSetString("created_at", lua.LString(r.CreatedAt.Format(time.RFC3339)))
|
|
arr.RawSetInt(i+1, ft)
|
|
}
|
|
L.Push(arr)
|
|
return 1
|
|
}))
|
|
|
|
tbl.RawSetString("read", L.NewFunction(func(L *lua.LState) int {
|
|
if svc == nil || svc.FilesSvc == nil {
|
|
return pushError(L, fmt.Errorf("file service not available"))
|
|
}
|
|
id := L.CheckString(1)
|
|
content, err := svc.FilesSvc.ReadText(id)
|
|
if err != nil {
|
|
return pushError(L, fmt.Errorf("file.read: %w", err))
|
|
}
|
|
L.Push(lua.LString(content))
|
|
return 1
|
|
}))
|
|
|
|
return tbl
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper: goValueToLua converts a Go interface{} (from sql.Scan) to lua.LValue.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func goValueToLua(L *lua.LState, v interface{}) lua.LValue {
|
|
if v == nil {
|
|
return lua.LNil
|
|
}
|
|
switch val := v.(type) {
|
|
case []byte:
|
|
return lua.LString(string(val))
|
|
case string:
|
|
return lua.LString(val)
|
|
case int64:
|
|
return lua.LNumber(val)
|
|
case float64:
|
|
return lua.LNumber(val)
|
|
case bool:
|
|
return lua.LBool(val)
|
|
case time.Time:
|
|
return lua.LString(val.Format(time.RFC3339))
|
|
default:
|
|
return lua.LString(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|