From ecae3e601ee60e7a517b3989eaa513154df59c29 Mon Sep 17 00:00:00 2001 From: Jo Date: Tue, 9 Jun 2026 00:31:26 +0200 Subject: [PATCH] Add Lua configuration support (#2845) * lua opt trial * go mod tidy --- .gitignore | 5 +- doc/init.lua | 53 ++++++++++++++ doc/lua.md | 49 +++++++++++++ go.mod | 1 + go.sum | 2 + main.go | 10 +++ pkg/settings/config.go | 102 +++++++++++++-------------- pkg/settings/dirs.go | 28 +++++++- pkg/settings/dirs_test.go | 26 +++++++ pkg/settings/lua/load.go | 36 ++++++++++ pkg/settings/lua/load_test.go | 56 +++++++++++++++ pkg/settings/lua/lua.go | 127 ++++++++++++++++++++++++++++++++++ pkg/settings/lua/lua_test.go | 83 ++++++++++++++++++++++ 13 files changed, 525 insertions(+), 53 deletions(-) create mode 100644 doc/init.lua create mode 100644 doc/lua.md create mode 100644 pkg/settings/lua/load.go create mode 100644 pkg/settings/lua/load_test.go create mode 100644 pkg/settings/lua/lua.go create mode 100644 pkg/settings/lua/lua_test.go diff --git a/.gitignore b/.gitignore index 22e4e8a1..adcf1574 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ qemu-* node_modules/ xgotext -.devcontainer/ \ No newline at end of file +.devcontainer/ +init.lua +dyalpm/ +!doc/init.lua \ No newline at end of file diff --git a/doc/init.lua b/doc/init.lua new file mode 100644 index 00000000..b9eca486 --- /dev/null +++ b/doc/init.lua @@ -0,0 +1,53 @@ +-- Example yay init.lua +-- +-- This file is a complete template for yay.opt. Copy entries you need, +-- or keep all of them and tune values. Command-line flags still override +-- these values. + +-- Strings +yay.opt.aururl = "https://aur.archlinux.org" -- Base AUR URL. +yay.opt.aurrpcurl = "https://aur.archlinux.org/rpc?" -- AUR RPC endpoint URL. +yay.opt.build_dir = os.getenv("HOME") .. "/.cache/yay" -- Build/cache directory for AUR packages. +yay.opt.editor = os.getenv("EDITOR") or os.getenv("VISUAL") or "vi" -- Editor command used for PKGBUILD edits; empty uses VISUAL/EDITOR. +yay.opt.editor_flags = "" -- Extra flags passed to the editor command. +yay.opt.makepkg_bin = "makepkg" -- makepkg executable (name in PATH or absolute path). +yay.opt.makepkg_conf = "" -- makepkg.conf path; empty uses default makepkg config. +yay.opt.pacman_bin = "pacman" -- pacman executable. +yay.opt.pacman_conf = "/etc/pacman.conf" -- pacman.conf file path. +yay.opt.redownload = "no" -- PKGBUILD download mode: "no" | "yes" | "all". +yay.opt.git_bin = "git" -- git executable. +yay.opt.gpg_bin = "gpg" -- gpg executable. +yay.opt.gpg_flags = "" -- Extra flags passed to gpg. +yay.opt.mflags = "" -- Extra flags passed to makepkg. +yay.opt.sort_by = "" -- AUR search sort field: "votes" | "popularity" | "name" | "base" | "submitted" | "modified" | "". +yay.opt.search_by = "name-desc" -- AUR search field: "name" | "name-desc" | "maintainer" | "depends" | "checkdepends" | "makedepends" | "optdepends" | "provides" | "conflicts" | "replaces" | "groups" | "keywords" | "comaintainers". +yay.opt.git_flags = "" -- Extra flags passed to git. +yay.opt.remove_make = "ask" -- Remove makedepends mode: "no" | "yes" | "ask" | "askyes". +yay.opt.sudo_bin = "sudo" -- Privilege elevation command. +yay.opt.sudo_flags = "" -- Extra flags passed to the sudo command. +yay.opt.rebuild = "no" -- Build mode: "no" | "yes" | "tree" | "all". + +-- Integers +yay.opt.request_split_n = 150 -- Max packages per AUR RPC request (use values > 0). +yay.opt.completion_refresh_time = 7 -- Completion cache refresh days: -1 (never), 0 (always), >0 (every N days). +yay.opt.max_concurrent_downloads = 1 -- Parallel PKGBUILD source downloads; 0 uses CPU count. + +-- Booleans +yay.opt.bottom_up = true -- Show AUR packages before repo packages in mixed results. +yay.opt.sudo_loop = false -- Keep sudo session alive in the background during long builds. +yay.opt.devel = false -- Check development/VCS packages on sysupgrade. +yay.opt.clean_after = false -- Remove untracked files after install. +yay.opt.keep_src = false -- Keep pkg/ and src/ after successful builds. +yay.opt.provides = true -- Resolve matching providers when dependencies are ambiguous. +yay.opt.pgp_fetch = true -- Prompt to import unknown PGP keys from validpgpkeys. +yay.opt.clean_menu = true -- Show pre-build clean menu. +yay.opt.diff_menu = true -- Show diff menu before building. +yay.opt.edit_menu = false -- Show PKGBUILD edit menu before building. +yay.opt.combined_upgrade = true -- Use combined repo+AUR upgrade flow on sysupgrade. +yay.opt.use_ask = false -- Use pacman's --ask to auto-confirm known conflicts. +yay.opt.batch_install = false -- Queue AUR package installs instead of installing each package immediately. +yay.opt.single_line_results = false -- Use single-line search result format. +yay.opt.separate_sources = true -- Separate query results by source (repo vs AUR). +yay.opt.debug = false -- Enable debug logging and local init.lua lookup convenience. +yay.opt.rpc = true -- Use AUR RPC for dependency/query operations. +yay.opt.double_confirm = true -- Ask for confirmation before and after builds during upgrades. \ No newline at end of file diff --git a/doc/lua.md b/doc/lua.md new file mode 100644 index 00000000..a6dee592 --- /dev/null +++ b/doc/lua.md @@ -0,0 +1,49 @@ +# Lua configuration (`init.lua`) + +yay can optionally load a Lua configuration file, `init.lua`. `init.lua` overlays whatever is in `config.json`, and any command-line flag you pass still wins over `init.lua`. + +## Location + +`init.lua` is looked up, in order: + +1. `$XDG_CONFIG_HOME/yay/init.lua` +2. `$HOME/.config/yay/init.lua` + +## Setting options with `yay.opt` + +Assign to keys on the `yay.opt` table using the exact option names shown +below. + +```lua +yay.opt.bottom_up = false +yay.opt.clean_after = true +yay.opt.sort_by = "votes" +yay.opt.request_split_n = 150 +yay.opt.editor = os.getenv("EDITOR") or "vi" +``` + +Unknown keys and values of the wrong type are treated as errors. yay aborts +startup and reports the offending keys/values so misconfigurations fail fast. + +### Available options + +**Strings** + +`aururl`, `aurrpcurl`, `build_dir`, `editor`, `editor_flags`, `makepkg_bin`, +`makepkg_conf`, `pacman_bin`, `pacman_conf`, `redownload`, `rebuild`, `git_bin`, +`gpg_bin`, `gpg_flags`, `mflags`, `sort_by`, `search_by`, `git_flags`, +`remove_make`, `sudo_bin`, `sudo_flags` + +**Integers** + +`request_split_n`, `completion_refresh_time`, `max_concurrent_downloads` + +**Booleans** + +`bottom_up`, `sudo_loop`, `devel`, `clean_after`, `keep_src`, `provides`, +`pgp_fetch`, `clean_menu`, `diff_menu`, `edit_menu`, `combined_upgrade`, +`use_ask`, `batch_install`, `single_line_results`, `separate_sources`, `debug`, +`rpc`, `double_confirm` + +A ready-to-copy example +lives at [`doc/init.lua`](init.lua). diff --git a/go.mod b/go.mod index af44e3b0..86f82e97 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/leonelquinteros/gotext v1.7.2 github.com/stretchr/testify v1.11.1 + github.com/yuin/gopher-lua v1.1.2 golang.org/x/net v0.55.0 golang.org/x/sys v0.45.0 golang.org/x/term v0.43.0 diff --git a/go.sum b/go.sum index 85a068c1..9ab65d9d 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= +github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= diff --git a/main.go b/main.go index 46ae9e9d..6ee05ec7 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "github.com/Jguer/yay/v12/pkg/db/ialpm" "github.com/Jguer/yay/v12/pkg/runtime" "github.com/Jguer/yay/v12/pkg/settings" + "github.com/Jguer/yay/v12/pkg/settings/lua" "github.com/Jguer/yay/v12/pkg/settings/parser" "github.com/Jguer/yay/v12/pkg/text" ) @@ -85,6 +86,15 @@ func main() { fallbackLog.Errorln(errS) } + if luaPath := settings.GetLuaConfigPath(cfg.Debug); luaPath != "" { + if errLua := lua.LoadInto(fallbackLog, luaPath, cfg); errLua != nil { + fallbackLog.Errorln(errLua) + ret = 1 + + return + } + } + cmdArgs := parser.MakeArguments() // Parse command line diff --git a/pkg/settings/config.go b/pkg/settings/config.go index 115047ba..5d75eb23 100644 --- a/pkg/settings/config.go +++ b/pkg/settings/config.go @@ -23,59 +23,59 @@ var NoConfirm = false // Configuration stores yay's config. type Configuration struct { - AURURL string `json:"aururl"` - AURRPCURL string `json:"aurrpcurl"` - BuildDir string `json:"buildDir"` - Editor string `json:"editor"` - EditorFlags string `json:"editorflags"` - MakepkgBin string `json:"makepkgbin"` - MakepkgConf string `json:"makepkgconf"` - PacmanBin string `json:"pacmanbin"` - PacmanConf string `json:"pacmanconf"` - ReDownload string `json:"redownload"` - AnswerClean string `json:"answerclean"` - AnswerDiff string `json:"answerdiff"` - AnswerEdit string `json:"answeredit"` - AnswerUpgrade string `json:"answerupgrade"` - GitBin string `json:"gitbin"` - GpgBin string `json:"gpgbin"` - GpgFlags string `json:"gpgflags"` - MFlags string `json:"mflags"` - SortBy string `json:"sortby"` - SearchBy string `json:"searchby"` - GitFlags string `json:"gitflags"` - RemoveMake string `json:"removemake"` - SudoBin string `json:"sudobin"` - SudoFlags string `json:"sudoflags"` - Version string `json:"version"` - RequestSplitN int `json:"requestsplitn"` - CompletionInterval int `json:"completionrefreshtime"` - MaxConcurrentDownloads int `json:"maxconcurrentdownloads"` - BottomUp bool `json:"bottomup"` - SudoLoop bool `json:"sudoloop"` - Devel bool `json:"devel"` - CleanAfter bool `json:"cleanAfter"` - KeepSrc bool `json:"keepSrc"` - Provides bool `json:"provides"` - PGPFetch bool `json:"pgpfetch"` - CleanMenu bool `json:"cleanmenu"` - DiffMenu bool `json:"diffmenu"` - EditMenu bool `json:"editmenu"` - CombinedUpgrade bool `json:"combinedupgrade"` - UseAsk bool `json:"useask"` - BatchInstall bool `json:"batchinstall"` - SingleLineResults bool `json:"singlelineresults"` - SeparateSources bool `json:"separatesources"` - Debug bool `json:"debug"` - UseRPC bool `json:"rpc"` - DoubleConfirm bool `json:"doubleconfirm"` // confirm install before and after build + AURURL string `json:"aururl" lua:"aururl"` + AURRPCURL string `json:"aurrpcurl" lua:"aurrpcurl"` + BuildDir string `json:"buildDir" lua:"build_dir"` + Editor string `json:"editor" lua:"editor"` + EditorFlags string `json:"editorflags" lua:"editor_flags"` + MakepkgBin string `json:"makepkgbin" lua:"makepkg_bin"` + MakepkgConf string `json:"makepkgconf" lua:"makepkg_conf"` + PacmanBin string `json:"pacmanbin" lua:"pacman_bin"` + PacmanConf string `json:"pacmanconf" lua:"pacman_conf"` + ReDownload string `json:"redownload" lua:"redownload"` + AnswerClean string `json:"answerclean" lua:"-"` + AnswerDiff string `json:"answerdiff" lua:"-"` + AnswerEdit string `json:"answeredit" lua:"-"` + AnswerUpgrade string `json:"answerupgrade" lua:"-"` + GitBin string `json:"gitbin" lua:"git_bin"` + GpgBin string `json:"gpgbin" lua:"gpg_bin"` + GpgFlags string `json:"gpgflags" lua:"gpg_flags"` + MFlags string `json:"mflags" lua:"mflags"` + SortBy string `json:"sortby" lua:"sort_by"` + SearchBy string `json:"searchby" lua:"search_by"` + GitFlags string `json:"gitflags" lua:"git_flags"` + RemoveMake string `json:"removemake" lua:"remove_make"` + SudoBin string `json:"sudobin" lua:"sudo_bin"` + SudoFlags string `json:"sudoflags" lua:"sudo_flags"` + Version string `json:"version" lua:"-"` + RequestSplitN int `json:"requestsplitn" lua:"request_split_n"` + CompletionInterval int `json:"completionrefreshtime" lua:"completion_refresh_time"` + MaxConcurrentDownloads int `json:"maxconcurrentdownloads" lua:"max_concurrent_downloads"` + BottomUp bool `json:"bottomup" lua:"bottom_up"` + SudoLoop bool `json:"sudoloop" lua:"sudo_loop"` + Devel bool `json:"devel" lua:"devel"` + CleanAfter bool `json:"cleanAfter" lua:"clean_after"` + KeepSrc bool `json:"keepSrc" lua:"keep_src"` + Provides bool `json:"provides" lua:"provides"` + PGPFetch bool `json:"pgpfetch" lua:"pgp_fetch"` + CleanMenu bool `json:"cleanmenu" lua:"clean_menu"` + DiffMenu bool `json:"diffmenu" lua:"diff_menu"` + EditMenu bool `json:"editmenu" lua:"edit_menu"` + CombinedUpgrade bool `json:"combinedupgrade" lua:"combined_upgrade"` + UseAsk bool `json:"useask" lua:"use_ask"` + BatchInstall bool `json:"batchinstall" lua:"batch_install"` + SingleLineResults bool `json:"singlelineresults" lua:"single_line_results"` + SeparateSources bool `json:"separatesources" lua:"separate_sources"` + Debug bool `json:"debug" lua:"debug"` + UseRPC bool `json:"rpc" lua:"rpc"` + DoubleConfirm bool `json:"doubleconfirm" lua:"double_confirm"` // confirm install before and after build - CompletionPath string `json:"-"` - VCSFilePath string `json:"-"` + CompletionPath string `json:"-" lua:"-"` + VCSFilePath string `json:"-" lua:"-"` // ConfigPath string `json:"-"` - SaveConfig bool `json:"-"` - Mode parser.TargetMode `json:"-"` - ReBuild parser.RebuildMode `json:"rebuild"` + SaveConfig bool `json:"-" lua:"-"` + Mode parser.TargetMode `json:"-" lua:"-"` + ReBuild parser.RebuildMode `json:"rebuild" lua:"rebuild"` } // SaveConfig writes yay config to file. diff --git a/pkg/settings/dirs.go b/pkg/settings/dirs.go index ceb52fb0..b054a90a 100644 --- a/pkg/settings/dirs.go +++ b/pkg/settings/dirs.go @@ -7,7 +7,8 @@ import ( const ( configFileName string = "config.json" // configFileName holds the name of the config file. - vcsFileName string = "vcs.json" // vcsFileName holds the name of the vcs file. + luaConfigFileName string = "init.lua" + vcsFileName string = "vcs.json" // vcsFileName holds the name of the vcs file. completionFileName string = "completion.cache" systemdCache string = "/var/cache/yay" // systemd should handle cache creation ) @@ -30,6 +31,31 @@ func GetConfigPath() string { return "" } +// GetLuaConfigPath returns the first configured init.lua path that exists. +func GetLuaConfigPath(debug bool) string { + var candidates []string + + if debug { + candidates = append(candidates, luaConfigFileName) + } + + if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" { + candidates = append(candidates, filepath.Join(configHome, "yay", luaConfigFileName)) + } + + if configHome := os.Getenv("HOME"); configHome != "" { + candidates = append(candidates, filepath.Join(configHome, ".config", "yay", luaConfigFileName)) + } + + for _, candidate := range candidates { + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + + return "" +} + func getCacheHome() (string, error) { uid := os.Geteuid() diff --git a/pkg/settings/dirs_test.go b/pkg/settings/dirs_test.go index 582b9459..04813cba 100644 --- a/pkg/settings/dirs_test.go +++ b/pkg/settings/dirs_test.go @@ -29,3 +29,29 @@ func Test_getCacheHome(t *testing.T) { require.NoError(t, os.Unsetenv("TMPDIR")) require.NoError(t, os.Unsetenv("SUDO_USER")) } + +func TestGetLuaConfigPath(t *testing.T) { + configHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", configHome) + require.NoError(t, os.Unsetenv("HOME")) + + assert.Empty(t, GetLuaConfigPath(false)) + + luaDir := filepath.Join(configHome, "yay") + require.NoError(t, os.MkdirAll(luaDir, 0o755)) + luaPath := filepath.Join(luaDir, "init.lua") + require.NoError(t, os.WriteFile(luaPath, []byte("-- test"), 0o600)) + + assert.Equal(t, luaPath, GetLuaConfigPath(false)) +} + +func TestGetLuaConfigPathDebugCwd(t *testing.T) { + require.NoError(t, os.Unsetenv("XDG_CONFIG_HOME")) + require.NoError(t, os.Unsetenv("HOME")) + + t.Chdir(t.TempDir()) + require.NoError(t, os.WriteFile("init.lua", []byte("-- test"), 0o600)) + + assert.Equal(t, "init.lua", GetLuaConfigPath(true)) + assert.Empty(t, GetLuaConfigPath(false)) +} diff --git a/pkg/settings/lua/load.go b/pkg/settings/lua/load.go new file mode 100644 index 00000000..6dce940f --- /dev/null +++ b/pkg/settings/lua/load.go @@ -0,0 +1,36 @@ +package lua + +import ( + "fmt" + + "github.com/Jguer/yay/v12/pkg/multierror" + "github.com/Jguer/yay/v12/pkg/text" +) + +// LoadInto applies the yay.opt values from path onto cfg. +func LoadInto(_ *text.Logger, path string, cfg any) error { + engine := New() + defer engine.Close() + + if err := engine.L.DoFile(path); err != nil { + return err + } + + unknown, errs := engine.Apply(cfg) + + if len(unknown) == 0 && len(errs) == 0 { + return nil + } + + merr := &multierror.MultiError{} + + for _, key := range unknown { + merr.Add(fmt.Errorf("init.lua: unknown yay.opt key: %s", key)) + } + + for _, err := range errs { + merr.Add(fmt.Errorf("init.lua: %w", err)) + } + + return merr.Return() +} diff --git a/pkg/settings/lua/load_test.go b/pkg/settings/lua/load_test.go new file mode 100644 index 00000000..0c428313 --- /dev/null +++ b/pkg/settings/lua/load_test.go @@ -0,0 +1,56 @@ +package lua + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func writeLuaFile(t *testing.T, body string) string { + t.Helper() + + dir := t.TempDir() + path := filepath.Join(dir, "init.lua") + require.NoError(t, os.WriteFile(path, []byte(body), 0o600)) + + return path +} + +func TestLoadIntoStrictFailsOnUnknownKey(t *testing.T) { + path := writeLuaFile(t, ` + yay.opt.unknown_key = true + `) + + cfg := &testConfig{} + err := LoadInto(nil, path, cfg) + require.Error(t, err) + require.Contains(t, err.Error(), "unknown yay.opt key: unknown_key") +} + +func TestLoadIntoStrictFailsOnTypeMismatch(t *testing.T) { + path := writeLuaFile(t, ` + yay.opt.devel = "true" + `) + + cfg := &testConfig{} + err := LoadInto(nil, path, cfg) + require.Error(t, err) + require.Contains(t, err.Error(), "yay.opt.devel: expected boolean") +} + +func TestLoadIntoAppliesValidValues(t *testing.T) { + path := writeLuaFile(t, ` + yay.opt.build_dir = "/tmp/yay" + yay.opt.request_split_n = 123 + yay.opt.devel = true + `) + + cfg := &testConfig{} + err := LoadInto(nil, path, cfg) + require.NoError(t, err) + require.Equal(t, "/tmp/yay", cfg.BuildDir) + require.Equal(t, 123, cfg.RequestSplitN) + require.True(t, cfg.Devel) +} diff --git a/pkg/settings/lua/lua.go b/pkg/settings/lua/lua.go new file mode 100644 index 00000000..4e0799ea --- /dev/null +++ b/pkg/settings/lua/lua.go @@ -0,0 +1,127 @@ +// Package lua loads yay's optional init.lua configuration. +package lua + +import ( + "fmt" + "reflect" + + lua "github.com/yuin/gopher-lua" +) + +const ( + globalName = "yay" + optTableName = "opt" +) + +type Engine struct { + L *lua.LState +} + +func New() *Engine { + state := lua.NewState() + + yayTbl := state.NewTable() + state.SetGlobal(globalName, yayTbl) + state.SetField(yayTbl, optTableName, state.NewTable()) + + return &Engine{L: state} +} + +func (e *Engine) Close() { + e.L.Close() +} + +// Apply writes recognized yay.opt values into cfg. +func (e *Engine) Apply(cfg any) (unknown []string, errs []error) { + v := reflect.ValueOf(cfg) + if v.Kind() != reflect.Pointer || v.Elem().Kind() != reflect.Struct { + return nil, []error{fmt.Errorf("lua: Apply expected pointer to struct, got %T", cfg)} + } + + sv := v.Elem() + st := sv.Type() + + index := make(map[string]int, st.NumField()) + + for i := range st.NumField() { + field := st.Field(i) + if name := luaKeyForField(&field); name != "" { + index[name] = i + } + } + + optTbl, ok := e.optTable() + if !ok { + return nil, nil + } + + optTbl.ForEach(func(k, val lua.LValue) { + key, ok := k.(lua.LString) + if !ok { + return + } + + fieldIdx, found := index[string(key)] + if !found { + unknown = append(unknown, string(key)) + + return + } + + if err := assign(sv.Field(fieldIdx), val); err != nil { + errs = append(errs, fmt.Errorf("yay.opt.%s: %w", string(key), err)) + } + }) + + return unknown, errs +} + +func (e *Engine) optTable() (*lua.LTable, bool) { + yayTbl, ok := e.L.GetGlobal(globalName).(*lua.LTable) + if !ok { + return nil, false + } + + optTbl, ok := e.L.GetField(yayTbl, optTableName).(*lua.LTable) + + return optTbl, ok +} + +func luaKeyForField(field *reflect.StructField) string { + name := field.Tag.Get("lua") + if name != "" && name != "-" { + return name + } + + return "" +} + +func assign(field reflect.Value, val lua.LValue) error { + switch field.Kind() { + case reflect.String: + s, ok := val.(lua.LString) + if !ok { + return fmt.Errorf("expected string, got %s", val.Type()) + } + + field.SetString(string(s)) + case reflect.Bool: + b, ok := val.(lua.LBool) + if !ok { + return fmt.Errorf("expected boolean, got %s", val.Type()) + } + + field.SetBool(bool(b)) + case reflect.Int, reflect.Int64: + n, ok := val.(lua.LNumber) + if !ok { + return fmt.Errorf("expected number, got %s", val.Type()) + } + + field.SetInt(int64(n)) + default: + return fmt.Errorf("unsupported field kind %s", field.Kind()) + } + + return nil +} diff --git a/pkg/settings/lua/lua_test.go b/pkg/settings/lua/lua_test.go new file mode 100644 index 00000000..0be24b60 --- /dev/null +++ b/pkg/settings/lua/lua_test.go @@ -0,0 +1,83 @@ +package lua + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testConfig struct { + BuildDir string `json:"buildDir" lua:"build_dir"` + RequestSplitN int `json:"requestsplitn" lua:"request_split_n"` + Devel bool `json:"devel" lua:"devel"` + AnswerClean string `json:"answerclean"` + Ignored string `json:"-" lua:"-"` +} + +func TestApply(t *testing.T) { + e := New() + defer e.Close() + + require.NoError(t, e.L.DoString(` + yay.opt.build_dir = "/tmp/yay" + yay.opt.request_split_n = 200 + yay.opt.devel = true + `)) + + cfg := &testConfig{} + unknown, errs := e.Apply(cfg) + + assert.Empty(t, unknown) + assert.Empty(t, errs) + assert.Equal(t, "/tmp/yay", cfg.BuildDir) + assert.Equal(t, 200, cfg.RequestSplitN) + assert.True(t, cfg.Devel) +} + +func TestApplyUnknownAndTypeMismatch(t *testing.T) { + e := New() + defer e.Close() + + require.NoError(t, e.L.DoString(` + yay.opt.does_not_exist = "x" + yay.opt.buildDir = "/tmp/nope" + yay.opt.requestsplitn = 200 + yay.opt.Devel = true + yay.opt.devel = "not a bool" + yay.opt.build_dir = "/tmp/ok" + `)) + + cfg := &testConfig{} + unknown, errs := e.Apply(cfg) + + assert.ElementsMatch(t, []string{"does_not_exist", "buildDir", "requestsplitn", "Devel"}, unknown) + assert.Len(t, errs, 1) + assert.Zero(t, cfg.RequestSplitN) + assert.False(t, cfg.Devel) + assert.Equal(t, "/tmp/ok", cfg.BuildDir) +} + +func TestApplyIgnoresAnswerOptionsWithoutLuaTags(t *testing.T) { + e := New() + defer e.Close() + + require.NoError(t, e.L.DoString(` + yay.opt.answer_clean = "All" + `)) + + cfg := &testConfig{} + unknown, errs := e.Apply(cfg) + + assert.Equal(t, []string{"answer_clean"}, unknown) + assert.Empty(t, errs) + assert.Empty(t, cfg.AnswerClean) +} + +func TestApplyRejectsNonPointer(t *testing.T) { + e := New() + defer e.Close() + + _, errs := e.Apply(testConfig{}) + assert.Len(t, errs, 1) +}