From 5a75b3e7cdb9ae7f71967a6de991a1cb0963ccb6 Mon Sep 17 00:00:00 2001 From: NightStar Date: Tue, 28 Jul 2026 15:50:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20config=20system=20=E2=80=94=20TOML=20lo?= =?UTF-8?q?ad/save=20with=20provider=20auth=20map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 5 +- go.sum | 2 + internal/config/config.go | 67 ++++++++++++++++++++++++ internal/config/config_test.go | 96 ++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go diff --git a/go.mod b/go.mod index 5d9bcc4..654f5f4 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module git.yoresee.cc/nightstar/ry go 1.18 -require github.com/spf13/cobra v1.10.2 +require ( + github.com/BurntSushi/toml v1.6.0 + github.com/spf13/cobra v1.10.2 +) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index a6ee3e0..c09a7f6 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..6899bc5 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,67 @@ +package config + +import ( + "os" + "path/filepath" + + "github.com/BurntSushi/toml" +) + +type ProviderConfig struct { + APIKey string `toml:"api_key,omitempty"` + AccessKeyID string `toml:"access_key_id,omitempty"` + AccessKeySecret string `toml:"access_key_secret,omitempty"` + Endpoint string `toml:"endpoint,omitempty"` +} + +type Config struct { + CurrentProvider string `toml:"current_provider"` + Output string `toml:"output"` + Providers map[string]ProviderConfig `toml:"providers"` +} + +func Load(path string) (*Config, error) { + cfg := &Config{ + Output: "table", + Providers: make(map[string]ProviderConfig), + } + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return cfg, nil + } + return nil, err + } + if err := toml.Unmarshal(data, cfg); err != nil { + return nil, err + } + if cfg.Providers == nil { + cfg.Providers = make(map[string]ProviderConfig) + } + if cfg.Output == "" { + cfg.Output = "table" + } + return cfg, nil +} + +func Save(path string, cfg *Config) error { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + enc := toml.NewEncoder(f) + return enc.Encode(cfg) +} + +func DefaultPath() string { + home, err := os.UserHomeDir() + if err != nil { + return "config.toml" + } + return filepath.Join(home, ".config", "ry", "config.toml") +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..baf41f1 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,96 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadTOML(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.toml") + content := ` +current_provider = "rainyun" +output = "table" + +[providers.rainyun] +api_key = "sk-test123" + +[providers.aliyun] +access_key_id = "ak-xxx" +access_key_secret = "sk-xxx" +` + if err := os.WriteFile(path, []byte(content), 0600); err != nil { + t.Fatal(err) + } + + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load() error: %v", err) + } + if cfg.CurrentProvider != "rainyun" { + t.Errorf("CurrentProvider = %q, want %q", cfg.CurrentProvider, "rainyun") + } + if cfg.Output != "table" { + t.Errorf("Output = %q, want %q", cfg.Output, "table") + } + if cfg.Providers["rainyun"].APIKey != "sk-test123" { + t.Errorf("rainyun api_key = %q, want %q", cfg.Providers["rainyun"].APIKey, "sk-test123") + } + if cfg.Providers["aliyun"].AccessKeyID != "ak-xxx" { + t.Errorf("aliyun access_key_id = %q, want %q", cfg.Providers["aliyun"].AccessKeyID, "ak-xxx") + } +} + +func TestLoadDefaults(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "empty.toml") + if err := os.WriteFile(path, []byte(""), 0600); err != nil { + t.Fatal(err) + } + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load() error: %v", err) + } + if cfg.Output != "table" { + t.Errorf("default Output = %q, want %q", cfg.Output, "table") + } +} + +func TestSave(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "out.toml") + cfg := &Config{ + CurrentProvider: "test", + Output: "json", + Providers: map[string]ProviderConfig{ + "test": {APIKey: "secret"}, + }, + } + if err := Save(path, cfg); err != nil { + t.Fatalf("Save() error: %v", err) + } + loaded, err := Load(path) + if err != nil { + t.Fatalf("re-Load() error: %v", err) + } + if loaded.CurrentProvider != "test" { + t.Errorf("roundtrip CurrentProvider = %q", loaded.CurrentProvider) + } +} + +func TestDefaultPath(t *testing.T) { + path := DefaultPath() + if path == "" { + t.Error("DefaultPath() returned empty") + } +} + +func TestEnsureDir(t *testing.T) { + dir := filepath.Join(t.TempDir(), "sub", "dir") + path := filepath.Join(dir, "config.toml") + cfg := &Config{Output: "table"} + if err := Save(path, cfg); err != nil { + t.Fatalf("Save() to deep dir error: %v", err) + } +}