feat(forge): add config file persistence
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
edbf3f7088
commit
0dd5916f4e
2 changed files with 187 additions and 3 deletions
85
config.go
85
config.go
|
|
@ -1,9 +1,13 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -16,8 +20,62 @@ const (
|
|||
DefaultURL = "http://localhost:3000"
|
||||
)
|
||||
|
||||
const defaultConfigPath = ".config/forge/config.json"
|
||||
|
||||
type configFile struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func configPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", core.E("configPath", "forge: resolve home directory", err)
|
||||
}
|
||||
return filepath.Join(home, defaultConfigPath), nil
|
||||
}
|
||||
|
||||
func readConfigFile() (url, token string, err error) {
|
||||
path, err := configPath()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
data, err := coreio.Local.Read(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", "", nil
|
||||
}
|
||||
return "", "", core.E("ResolveConfig", "forge: read config file", err)
|
||||
}
|
||||
|
||||
var cfg configFile
|
||||
if err := json.Unmarshal([]byte(data), &cfg); err != nil {
|
||||
return "", "", core.E("ResolveConfig", "forge: decode config file", err)
|
||||
}
|
||||
return cfg.URL, cfg.Token, nil
|
||||
}
|
||||
|
||||
// SaveConfig persists the Forgejo URL and API token to the default config file.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// _ = forge.SaveConfig("https://forge.example.com", "token")
|
||||
func SaveConfig(url, token string) error {
|
||||
path, err := configPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := json.MarshalIndent(configFile{URL: url, Token: token}, "", " ")
|
||||
if err != nil {
|
||||
return core.E("SaveConfig", "forge: encode config file", err)
|
||||
}
|
||||
return coreio.Local.WriteMode(path, string(payload), 0600)
|
||||
}
|
||||
|
||||
// ResolveConfig resolves the Forgejo URL and API token from flags, environment
|
||||
// variables, and built-in defaults. Priority order: flags > env > defaults.
|
||||
// variables, config file, and built-in defaults. Priority order:
|
||||
// flags > env > config file > defaults.
|
||||
//
|
||||
// Environment variables:
|
||||
// - FORGE_URL — base URL of the Forgejo instance
|
||||
|
|
@ -29,8 +87,19 @@ const (
|
|||
// _ = url
|
||||
// _ = token
|
||||
func ResolveConfig(flagURL, flagToken string) (url, token string, err error) {
|
||||
url, _ = syscall.Getenv("FORGE_URL")
|
||||
token, _ = syscall.Getenv("FORGE_TOKEN")
|
||||
if fileURL, fileToken, fileErr := readConfigFile(); fileErr != nil {
|
||||
return "", "", fileErr
|
||||
} else {
|
||||
url = fileURL
|
||||
token = fileToken
|
||||
}
|
||||
|
||||
if envURL, ok := syscall.Getenv("FORGE_URL"); ok && envURL != "" {
|
||||
url = envURL
|
||||
}
|
||||
if envToken, ok := syscall.Getenv("FORGE_TOKEN"); ok && envToken != "" {
|
||||
token = envToken
|
||||
}
|
||||
|
||||
if flagURL != "" {
|
||||
url = flagURL
|
||||
|
|
@ -44,6 +113,16 @@ func ResolveConfig(flagURL, flagToken string) (url, token string, err error) {
|
|||
return url, token, nil
|
||||
}
|
||||
|
||||
// NewFromConfig creates a new Forge client using resolved configuration.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f, err := forge.NewFromConfig("", "")
|
||||
// _ = f
|
||||
func NewFromConfig(flagURL, flagToken string, opts ...Option) (*Forge, error) {
|
||||
return NewForgeFromConfig(flagURL, flagToken, opts...)
|
||||
}
|
||||
|
||||
// NewForgeFromConfig creates a new Forge client using resolved configuration.
|
||||
// It returns an error if no API token is available from flags or environment.
|
||||
//
|
||||
|
|
|
|||
105
config_test.go
105
config_test.go
|
|
@ -1,10 +1,15 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
coreio "dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
func TestResolveConfig_EnvOverrides_Good(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("FORGE_URL", "https://forge.example.com")
|
||||
t.Setenv("FORGE_TOKEN", "env-token")
|
||||
|
||||
|
|
@ -21,6 +26,7 @@ func TestResolveConfig_EnvOverrides_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResolveConfig_FlagOverridesEnv_Good(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("FORGE_URL", "https://env.example.com")
|
||||
t.Setenv("FORGE_TOKEN", "env-token")
|
||||
|
||||
|
|
@ -37,6 +43,7 @@ func TestResolveConfig_FlagOverridesEnv_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResolveConfig_DefaultURL_Good(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("FORGE_URL", "")
|
||||
t.Setenv("FORGE_TOKEN", "")
|
||||
|
||||
|
|
@ -49,7 +56,63 @@ func TestResolveConfig_DefaultURL_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolveConfig_ConfigFile_Good(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("FORGE_URL", "")
|
||||
t.Setenv("FORGE_TOKEN", "")
|
||||
|
||||
cfgPath := filepath.Join(home, ".config", "forge", "config.json")
|
||||
if err := coreio.Local.EnsureDir(filepath.Dir(cfgPath)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"url": "https://file.example.com",
|
||||
"token": "file-token",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := coreio.Local.WriteMode(cfgPath, string(data), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
url, token, err := ResolveConfig("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if url != "https://file.example.com" {
|
||||
t.Errorf("got url=%q", url)
|
||||
}
|
||||
if token != "file-token" {
|
||||
t.Errorf("got token=%q", token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveConfig_EnvOverridesConfig_Good(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("FORGE_URL", "https://env.example.com")
|
||||
t.Setenv("FORGE_TOKEN", "env-token")
|
||||
|
||||
if err := SaveConfig("https://file.example.com", "file-token"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
url, token, err := ResolveConfig("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if url != "https://env.example.com" {
|
||||
t.Errorf("got url=%q", url)
|
||||
}
|
||||
if token != "env-token" {
|
||||
t.Errorf("got token=%q", token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewForgeFromConfig_NoToken_Bad(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("FORGE_URL", "")
|
||||
t.Setenv("FORGE_TOKEN", "")
|
||||
|
||||
|
|
@ -58,3 +121,45 @@ func TestNewForgeFromConfig_NoToken_Bad(t *testing.T) {
|
|||
t.Fatal("expected error for missing token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFromConfig_Good(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("FORGE_URL", "https://forge.example.com")
|
||||
t.Setenv("FORGE_TOKEN", "env-token")
|
||||
|
||||
f, err := NewFromConfig("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f == nil {
|
||||
t.Fatal("expected forge client")
|
||||
}
|
||||
if got := f.BaseURL(); got != "https://forge.example.com" {
|
||||
t.Errorf("got baseURL=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveConfig_Good(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
|
||||
if err := SaveConfig("https://file.example.com", "file-token"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfgPath := filepath.Join(home, ".config", "forge", "config.json")
|
||||
data, err := coreio.Local.Read(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg map[string]string
|
||||
if err := json.Unmarshal([]byte(data), &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg["url"] != "https://file.example.com" {
|
||||
t.Errorf("got url=%q", cfg["url"])
|
||||
}
|
||||
if cfg["token"] != "file-token" {
|
||||
t.Errorf("got token=%q", cfg["token"])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue