Skip to content

Commit be9dd81

Browse files
committed
refactor: Clean config files & reduce package's public api surface
1 parent f043cbb commit be9dd81

File tree

4 files changed

+136
-159
lines changed

4 files changed

+136
-159
lines changed

pkg/config/config.go

Lines changed: 13 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
package config
22

33
import (
4-
"bytes"
5-
"fmt"
64
"os"
7-
"os/exec"
85
"path/filepath"
9-
"runtime"
10-
"strings"
116
"time"
127

13-
"github.com/BurntSushi/toml"
14-
"github.com/mitchellh/go-homedir"
158
log "github.com/sirupsen/logrus"
169
"github.com/spf13/viper"
1710
prefixed "github.com/x-cray/logrus-prefixed-formatter"
@@ -52,30 +45,6 @@ type Config struct {
5245
fs ConfigFS
5346
}
5447

55-
// getConfigFolder retrieves the folder where the profiles file is stored
56-
// It searches for the xdg environment path first and will secondarily
57-
// place it in the home directory
58-
func getConfigFolder(xdgPath string) string {
59-
configPath := xdgPath
60-
61-
if configPath == "" {
62-
home, err := homedir.Dir()
63-
if err != nil {
64-
fmt.Println(err)
65-
os.Exit(1)
66-
}
67-
68-
configPath = filepath.Join(home, ".config")
69-
}
70-
71-
log.WithFields(log.Fields{
72-
"prefix": "config.Config.GetProfilesFolder",
73-
"path": configPath,
74-
}).Debug("Using profiles folder")
75-
76-
return filepath.Join(configPath, "hookdeck")
77-
}
78-
7948
// InitConfig reads in profiles file and ENV variables if set.
8049
func (c *Config) InitConfig() {
8150
if c.fs == nil {
@@ -155,37 +124,6 @@ func (c *Config) InitConfig() {
155124
log.SetFormatter(logFormatter)
156125
}
157126

158-
// EditConfig opens the configuration file in the default editor.
159-
func (c *Config) EditConfig() error {
160-
var err error
161-
162-
fmt.Println("Opening config file:", c.configFile)
163-
164-
switch runtime.GOOS {
165-
case "darwin", "linux":
166-
editor := os.Getenv("EDITOR")
167-
if editor == "" {
168-
editor = "vi"
169-
}
170-
171-
cmd := exec.Command(editor, c.configFile)
172-
// Some editors detect whether they have control of stdin/out and will
173-
// fail if they do not.
174-
cmd.Stdin = os.Stdin
175-
cmd.Stdout = os.Stdout
176-
177-
return cmd.Run()
178-
case "windows":
179-
// As far as I can tell, Windows doesn't have an easily accesible or
180-
// comparable option to $EDITOR, so default to notepad for now
181-
err = exec.Command("notepad", c.configFile).Run()
182-
default:
183-
err = fmt.Errorf("unsupported platform")
184-
}
185-
186-
return err
187-
}
188-
189127
// UseProject selects the active project to be used
190128
func (c *Config) UseProject(teamId string, teamMode string) error {
191129
c.Profile.TeamID = teamId
@@ -228,10 +166,10 @@ func (c *Config) RemoveAllProfiles() error {
228166
runtimeViper.SetConfigType("toml")
229167
runtimeViper.SetConfigFile(c.viper.ConfigFileUsed())
230168
c.viper = runtimeViper
231-
return c.WriteConfig()
169+
return c.writeConfig()
232170
}
233171

234-
func (c *Config) WriteConfig() error {
172+
func (c *Config) writeConfig() error {
235173
if err := c.fs.makePath(c.viper.ConfigFileUsed()); err != nil {
236174
return err
237175
}
@@ -246,13 +184,13 @@ func (c *Config) WriteConfig() error {
246184

247185
// Construct the config struct from flags > local config > global config
248186
func (c *Config) constructConfig() {
249-
c.Color = getStringConfig([]string{c.Color, c.viper.GetString(("color")), "auto"})
250-
c.LogLevel = getStringConfig([]string{c.LogLevel, c.viper.GetString(("log")), "info"})
251-
c.APIBaseURL = getStringConfig([]string{c.APIBaseURL, c.viper.GetString(("api_base")), hookdeck.DefaultAPIBaseURL})
252-
c.DashboardBaseURL = getStringConfig([]string{c.DashboardBaseURL, c.viper.GetString(("dashboard_base")), hookdeck.DefaultDashboardBaseURL})
253-
c.ConsoleBaseURL = getStringConfig([]string{c.ConsoleBaseURL, c.viper.GetString(("console_base")), hookdeck.DefaultConsoleBaseURL})
254-
c.WSBaseURL = getStringConfig([]string{c.WSBaseURL, c.viper.GetString(("ws_base")), hookdeck.DefaultWebsocektURL})
255-
c.Profile.Name = getStringConfig([]string{c.Profile.Name, c.viper.GetString(("profile")), hookdeck.DefaultProfileName})
187+
c.Color = stringCoalesce(c.Color, c.viper.GetString(("color")), "auto")
188+
c.LogLevel = stringCoalesce(c.LogLevel, c.viper.GetString(("log")), "info")
189+
c.APIBaseURL = stringCoalesce(c.APIBaseURL, c.viper.GetString(("api_base")), hookdeck.DefaultAPIBaseURL)
190+
c.DashboardBaseURL = stringCoalesce(c.DashboardBaseURL, c.viper.GetString(("dashboard_base")), hookdeck.DefaultDashboardBaseURL)
191+
c.ConsoleBaseURL = stringCoalesce(c.ConsoleBaseURL, c.viper.GetString(("console_base")), hookdeck.DefaultConsoleBaseURL)
192+
c.WSBaseURL = stringCoalesce(c.WSBaseURL, c.viper.GetString(("ws_base")), hookdeck.DefaultWebsocektURL)
193+
c.Profile.Name = stringCoalesce(c.Profile.Name, c.viper.GetString(("profile")), hookdeck.DefaultProfileName)
256194
// Needs to support both profile-based config
257195
// and top-level config for backward compat. For example:
258196
// ````
@@ -267,19 +205,9 @@ func (c *Config) constructConfig() {
267205
// "workspace" > "team"
268206
// TODO: use "project" instead of "workspace"
269207
// TODO: use "cli_key" instead of "api_key"
270-
c.Profile.APIKey = getStringConfig([]string{c.Profile.APIKey, c.viper.GetString(c.Profile.GetConfigField("api_key")), c.viper.GetString("api_key"), ""})
271-
c.Profile.TeamID = getStringConfig([]string{c.Profile.TeamID, c.viper.GetString(c.Profile.GetConfigField("workspace_id")), c.viper.GetString(c.Profile.GetConfigField("team_id")), c.viper.GetString("workspace_id"), ""})
272-
c.Profile.TeamMode = getStringConfig([]string{c.Profile.TeamMode, c.viper.GetString(c.Profile.GetConfigField("workspace_mode")), c.viper.GetString(c.Profile.GetConfigField("team_mode")), c.viper.GetString("workspace_mode"), ""})
273-
}
274-
275-
func getStringConfig(values []string) string {
276-
for _, str := range values {
277-
if str != "" {
278-
return str
279-
}
280-
}
281-
282-
return values[len(values)-1]
208+
c.Profile.APIKey = stringCoalesce(c.Profile.APIKey, c.viper.GetString(c.Profile.getConfigField("api_key")), c.viper.GetString("api_key"), "")
209+
c.Profile.TeamID = stringCoalesce(c.Profile.TeamID, c.viper.GetString(c.Profile.getConfigField("workspace_id")), c.viper.GetString(c.Profile.getConfigField("team_id")), c.viper.GetString("workspace_id"), "")
210+
c.Profile.TeamMode = stringCoalesce(c.Profile.TeamMode, c.viper.GetString(c.Profile.getConfigField("workspace_mode")), c.viper.GetString(c.Profile.getConfigField("team_mode")), c.viper.GetString("workspace_mode"), "")
283211
}
284212

285213
// getConfigPath returns the path for the config file.
@@ -310,69 +238,6 @@ func (c *Config) getConfigPath(path string) (string, bool) {
310238
return localConfigPath, false
311239
}
312240

313-
globalConfigFolder := getConfigFolder(os.Getenv("XDG_CONFIG_HOME"))
241+
globalConfigFolder := getSystemConfigFolder(os.Getenv("XDG_CONFIG_HOME"))
314242
return filepath.Join(globalConfigFolder, "config.toml"), true
315243
}
316-
317-
// isProfile identifies whether a value in the config pertains to a profile.
318-
func isProfile(value interface{}) bool {
319-
// TODO: ianjabour - ideally find a better way to identify projects in config
320-
_, ok := value.(map[string]interface{})
321-
return ok
322-
}
323-
324-
// Temporary workaround until https://github.com/spf13/viper/pull/519 can remove a key from viper
325-
func removeKey(v *viper.Viper, key string) (*viper.Viper, error) {
326-
configMap := v.AllSettings()
327-
path := strings.Split(key, ".")
328-
lastKey := strings.ToLower(path[len(path)-1])
329-
deepestMap := deepSearch(configMap, path[0:len(path)-1])
330-
delete(deepestMap, lastKey)
331-
332-
buf := new(bytes.Buffer)
333-
334-
encodeErr := toml.NewEncoder(buf).Encode(configMap)
335-
if encodeErr != nil {
336-
return nil, encodeErr
337-
}
338-
339-
nv := viper.New()
340-
nv.SetConfigType("toml") // hint to viper that we've encoded the data as toml
341-
342-
err := nv.ReadConfig(buf)
343-
if err != nil {
344-
return nil, err
345-
}
346-
347-
return nv, nil
348-
}
349-
350-
// taken from https://github.com/spf13/viper/blob/master/util.go#L199,
351-
// we need this to delete configs, remove when viper supprts unset natively
352-
func deepSearch(m map[string]interface{}, path []string) map[string]interface{} {
353-
for _, k := range path {
354-
m2, ok := m[k]
355-
if !ok {
356-
// intermediate key does not exist
357-
// => create it and continue from there
358-
m3 := make(map[string]interface{})
359-
m[k] = m3
360-
m = m3
361-
362-
continue
363-
}
364-
365-
m3, ok := m2.(map[string]interface{})
366-
if !ok {
367-
// intermediate key is a value
368-
// => replace with a new map
369-
m3 = make(map[string]interface{})
370-
m[k] = m3
371-
}
372-
373-
// continue search from here
374-
m = m3
375-
}
376-
377-
return m
378-
}

pkg/config/config_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestGetConfigPath(t *testing.T) {
3131
fs := &globalNoLocalConfigFS{}
3232
c := Config{fs: fs}
3333
customPathInput := ""
34-
expectedPath := filepath.Join(getConfigFolder(os.Getenv("XDG_CONFIG_HOME")), "config.toml")
34+
expectedPath := filepath.Join(getSystemConfigFolder(os.Getenv("XDG_CONFIG_HOME")), "config.toml")
3535

3636
path, isGlobalConfig := c.getConfigPath(customPathInput)
3737
assert.True(t, isGlobalConfig)
@@ -44,7 +44,7 @@ func TestGetConfigPath(t *testing.T) {
4444
fs := &noConfigFS{}
4545
c := Config{fs: fs}
4646
customPathInput := ""
47-
expectedPath := filepath.Join(getConfigFolder(os.Getenv("XDG_CONFIG_HOME")), "config.toml")
47+
expectedPath := filepath.Join(getSystemConfigFolder(os.Getenv("XDG_CONFIG_HOME")), "config.toml")
4848

4949
path, isGlobalConfig := c.getConfigPath(customPathInput)
5050
assert.True(t, isGlobalConfig)
@@ -255,7 +255,7 @@ func (fs *globalNoLocalConfigFS) makePath(path string) error {
255255
return nil
256256
}
257257
func (fs *globalNoLocalConfigFS) fileExists(path string) (bool, error) {
258-
globalConfigFolder := getConfigFolder(os.Getenv("XDG_CONFIG_HOME"))
258+
globalConfigFolder := getSystemConfigFolder(os.Getenv("XDG_CONFIG_HOME"))
259259
globalPath := filepath.Join(globalConfigFolder, "config.toml")
260260
if path == globalPath {
261261
return true, nil

pkg/config/helpers.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package config
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/BurntSushi/toml"
11+
"github.com/mitchellh/go-homedir"
12+
log "github.com/sirupsen/logrus"
13+
"github.com/spf13/viper"
14+
)
15+
16+
// getSystemConfigFolder retrieves the folder where the profiles file is stored
17+
// It searches for the xdg environment path first and will secondarily
18+
// place it in the home directory
19+
func getSystemConfigFolder(xdgPath string) string {
20+
configPath := xdgPath
21+
22+
if configPath == "" {
23+
home, err := homedir.Dir()
24+
if err != nil {
25+
fmt.Println(err)
26+
os.Exit(1)
27+
}
28+
29+
configPath = filepath.Join(home, ".config")
30+
}
31+
32+
log.WithFields(log.Fields{
33+
"prefix": "config.Config.GetProfilesFolder",
34+
"path": configPath,
35+
}).Debug("Using profiles folder")
36+
37+
return filepath.Join(configPath, "hookdeck")
38+
}
39+
40+
// isProfile identifies whether a value in the config pertains to a profile.
41+
func isProfile(value interface{}) bool {
42+
// TODO: ianjabour - ideally find a better way to identify projects in config
43+
_, ok := value.(map[string]interface{})
44+
return ok
45+
}
46+
47+
// Temporary workaround until https://github.com/spf13/viper/pull/519 can remove a key from viper
48+
func removeKey(v *viper.Viper, key string) (*viper.Viper, error) {
49+
configMap := v.AllSettings()
50+
path := strings.Split(key, ".")
51+
lastKey := strings.ToLower(path[len(path)-1])
52+
deepestMap := deepSearch(configMap, path[0:len(path)-1])
53+
delete(deepestMap, lastKey)
54+
55+
buf := new(bytes.Buffer)
56+
57+
encodeErr := toml.NewEncoder(buf).Encode(configMap)
58+
if encodeErr != nil {
59+
return nil, encodeErr
60+
}
61+
62+
nv := viper.New()
63+
nv.SetConfigType("toml") // hint to viper that we've encoded the data as toml
64+
65+
err := nv.ReadConfig(buf)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
return nv, nil
71+
}
72+
73+
// taken from https://github.com/spf13/viper/blob/master/util.go#L199,
74+
// we need this to delete configs, remove when viper supprts unset natively
75+
func deepSearch(m map[string]interface{}, path []string) map[string]interface{} {
76+
for _, k := range path {
77+
m2, ok := m[k]
78+
if !ok {
79+
// intermediate key does not exist
80+
// => create it and continue from there
81+
m3 := make(map[string]interface{})
82+
m[k] = m3
83+
m = m3
84+
85+
continue
86+
}
87+
88+
m3, ok := m2.(map[string]interface{})
89+
if !ok {
90+
// intermediate key is a value
91+
// => replace with a new map
92+
m3 = make(map[string]interface{})
93+
m[k] = m3
94+
}
95+
96+
// continue search from here
97+
m = m3
98+
}
99+
100+
return m
101+
}
102+
103+
// stringCoalesce returns the first non-empty string in the list of strings.
104+
func stringCoalesce(values ...string) string {
105+
for _, str := range values {
106+
if str != "" {
107+
return str
108+
}
109+
}
110+
111+
return values[len(values)-1]
112+
}

pkg/config/profile.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ type Profile struct {
1111
Config *Config
1212
}
1313

14-
// GetConfigField returns the configuration field for the specific profile
15-
func (p *Profile) GetConfigField(field string) string {
14+
// getConfigField returns the configuration field for the specific profile
15+
func (p *Profile) getConfigField(field string) string {
1616
return p.Name + "." + field
1717
}
1818

1919
func (p *Profile) SaveProfile() error {
20-
p.Config.viper.Set(p.GetConfigField("api_key"), p.APIKey)
21-
p.Config.viper.Set(p.GetConfigField("workspace_id"), p.TeamID)
22-
p.Config.viper.Set(p.GetConfigField("workspace_mode"), p.TeamMode)
23-
return p.Config.WriteConfig()
20+
p.Config.viper.Set(p.getConfigField("api_key"), p.APIKey)
21+
p.Config.viper.Set(p.getConfigField("workspace_id"), p.TeamID)
22+
p.Config.viper.Set(p.getConfigField("workspace_mode"), p.TeamMode)
23+
return p.Config.writeConfig()
2424
}
2525

2626
func (p *Profile) RemoveProfile() error {
@@ -39,12 +39,12 @@ func (p *Profile) RemoveProfile() error {
3939
runtimeViper.SetConfigType("toml")
4040
runtimeViper.SetConfigFile(p.Config.viper.ConfigFileUsed())
4141
p.Config.viper = runtimeViper
42-
return p.Config.WriteConfig()
42+
return p.Config.writeConfig()
4343
}
4444

4545
func (p *Profile) UseProfile() error {
4646
p.Config.viper.Set("profile", p.Name)
47-
return p.Config.WriteConfig()
47+
return p.Config.writeConfig()
4848
}
4949

5050
func (p *Profile) ValidateAPIKey() error {

0 commit comments

Comments
 (0)