commit 8dc7af5e3a108742a2ae781d13e1127692704b4d
parent ae288011354112aa746043c79e5c6c70154ef1e0
Author: Sean Enck <sean@ttypty.com>
Date: Sun, 28 Sep 2025 23:21:53 -0400
config loading and file handling can be a single function
Diffstat:
6 files changed, 109 insertions(+), 50 deletions(-)
diff --git a/cmd/lb/main.go b/cmd/lb/main.go
@@ -47,18 +47,8 @@ func handleEarly(command string, args []string) (bool, error) {
}
func run() error {
- cfg := app.ConfigLoader{}
- for _, p := range config.NewConfigFiles() {
- if cfg.Check(p) {
- r, err := cfg.Read(p)
- if err != nil {
- return err
- }
- if err := config.Load(r, cfg); err != nil {
- return err
- }
- break
- }
+ if err := config.Parse(app.ConfigLoader{}); err != nil {
+ return err
}
args := os.Args
if len(args) < 2 {
diff --git a/internal/app/core.go b/internal/app/core.go
@@ -99,3 +99,8 @@ func (c ConfigLoader) Read(file string) (io.Reader, error) {
func (c ConfigLoader) Check(file string) bool {
return platform.PathExists(file)
}
+
+// Home will read the user's home directory
+func (c ConfigLoader) Home() (string, error) {
+ return os.UserHomeDir()
+}
diff --git a/internal/config/core.go b/internal/config/core.go
@@ -3,6 +3,7 @@ package config
import (
"fmt"
+ "io"
"net/url"
"os"
"path/filepath"
@@ -91,24 +92,48 @@ type (
Text string
Position Position
}
+ // Loader indicates how config files should be read
+ Loader interface {
+ Read(string) (io.Reader, error)
+ Check(string) bool
+ Home() (string, error)
+ }
)
-// NewConfigFiles will get the list of candidate config files
-func NewConfigFiles() []string {
+// Parse will parse the config based on the loader
+func Parse(loader Loader) error {
+ process := func(path string) (bool, error) {
+ if loader.Check(path) {
+ r, err := loader.Read(path)
+ if err != nil {
+ return false, err
+ }
+ return true, Load(r, loader)
+ }
+ return false, nil
+ }
v := os.Expand(os.Getenv(ConfigEnv), os.Getenv)
if v != "" {
- return []string{v}
+ // NOTE: when this environment variable is set - either load the config or exit, the user does NOT want the default config options regardless
+ _, err := process(v)
+ return err
}
- var options []string
- pathAdder := func(root, sub string, err error) {
- if err == nil && root != "" {
- options = append(options, filepath.Join(root, sub))
+ pathAdder := func(root, sub string) (bool, error) {
+ if root != "" {
+ return process(filepath.Join(root, sub))
}
+ return false, nil
+ }
+ ok, err := pathAdder(os.Getenv("XDG_CONFIG_HOME"), ConfigXDG)
+ if ok || err != nil {
+ return err
+ }
+ h, err := loader.Home()
+ if err != nil {
+ return err
}
- pathAdder(os.Getenv("XDG_CONFIG_HOME"), ConfigXDG, nil)
- h, err := os.UserHomeDir()
- pathAdder(h, ConfigHome, err)
- return options
+ _, err = pathAdder(h, ConfigHome)
+ return err
}
func environmentRegister[T printer](obj T) T {
diff --git a/internal/config/core_test.go b/internal/config/core_test.go
@@ -1,37 +1,73 @@
package config_test
import (
+ "errors"
+ "io"
"os"
+ "strings"
"testing"
"github.com/enckse/lockbox/internal/config"
"github.com/enckse/lockbox/internal/config/store"
)
-func TestNewEnvFiles(t *testing.T) {
+func TestParse(t *testing.T) {
+ store.Clear()
+ defer store.Clear()
os.Clearenv()
+ defer os.Clearenv()
+ mock := mockReader{}
+ mock.home = func() (string, error) {
+ return "", errors.New("invalid home")
+ }
+ mock.check = func(string) bool {
+ return false
+ }
+ if err := config.Parse(mock); err == nil || err.Error() != "invalid home" {
+ t.Errorf("invalid error: %v", err)
+ }
+ mock.home = func() (string, error) {
+ return "hometest", nil
+ }
+ mock.read = func(p string) (io.Reader, error) {
+ if p == "hometest/.config/lockbox/config.toml" {
+ return strings.NewReader("[feature]\ncolor = true"), nil
+ }
+ return nil, nil
+ }
+ mock.check = func(string) bool {
+ return true
+ }
+ store.SetBool("LOCKBOX_FEATURE_COLOR", false)
+ if err := config.Parse(mock); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if ok, _ := store.GetBool("LOCKBOX_FEATURE_COLOR"); !ok {
+ t.Error("should have set")
+ }
+ t.Setenv("XDG_CONFIG_HOME", "xdghome")
+ mock.read = func(p string) (io.Reader, error) {
+ if p == "xdghome/lockbox/config.toml" {
+ return strings.NewReader("[feature]\ncolor = false"), nil
+ }
+ return nil, nil
+ }
+ if err := config.Parse(mock); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if ok, _ := store.GetBool("LOCKBOX_FEATURE_COLOR"); ok {
+ t.Error("should have unset")
+ }
+ store.SetBool("LOCKBOX_FEATURE_COLOR", true)
t.Setenv("LOCKBOX_CONFIG_TOML", "test")
- f := config.NewConfigFiles()
- if len(f) != 1 || f[0] != "test" {
- t.Errorf("invalid files: %v", f)
- }
- t.Setenv("HOME", "test")
- t.Setenv("LOCKBOX_CONFIG_TOML", "")
- f = config.NewConfigFiles()
- if len(f) != 1 {
- t.Errorf("invalid files: %v", f)
- }
- t.Setenv("LOCKBOX_CONFIG_TOML", "")
- t.Setenv("XDG_CONFIG_HOME", "test")
- f = config.NewConfigFiles()
- if len(f) != 2 {
- t.Errorf("invalid files: %v", f)
- }
- t.Setenv("LOCKBOX_CONFIG_TOML", "")
- os.Unsetenv("HOME")
- f = config.NewConfigFiles()
- if len(f) != 1 {
- t.Errorf("invalid files: %v", f)
+ mock.check = func(p string) bool {
+ return p != "test"
+ }
+ if err := config.Parse(mock); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if ok, _ := store.GetBool("LOCKBOX_FEATURE_COLOR"); !ok {
+ t.Error("should have set")
}
}
diff --git a/internal/config/toml.go b/internal/config/toml.go
@@ -27,11 +27,6 @@ const (
type (
tomlType string
- // Loader indicates how included files should be sourced
- Loader interface {
- Read(string) (io.Reader, error)
- Check(string) bool
- }
included struct {
value string
required bool
diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go
@@ -15,6 +15,7 @@ import (
type mockReader struct {
read func(string) (io.Reader, error)
check func(string) bool
+ home func() (string, error)
}
func (m mockReader) Read(path string) (io.Reader, error) {
@@ -31,6 +32,13 @@ func (m mockReader) Check(path string) bool {
return m.check(path)
}
+func (m mockReader) Home() (string, error) {
+ if m.home == nil {
+ return "", nil
+ }
+ return m.home()
+}
+
func TestLoadIncludes(t *testing.T) {
store.Clear()
defer os.Clearenv()