From f822ad1b83f088d32503107887081d8a3c2d9abc Mon Sep 17 00:00:00 2001 From: moyiz <8603313+moyiz@users.noreply.github.com> Date: Wed, 13 Dec 2023 20:19:30 +0200 Subject: [PATCH] refactor: refactor config management for testability --- cmd/add.go | 6 +-- cmd/list.go | 5 +- cmd/remove.go | 9 ++-- cmd/root.go | 2 +- cmd/run.go | 9 ++-- internal/cli/completion.go | 31 +++++++++++ internal/config/config.go | 108 ++++++++++++++++++------------------- internal/config/errors.go | 16 ++++++ 8 files changed, 115 insertions(+), 71 deletions(-) create mode 100644 internal/cli/completion.go create mode 100644 internal/config/errors.go diff --git a/cmd/add.go b/cmd/add.go index 3baee1a..608f3c4 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -27,7 +27,7 @@ It will create the config directory if at does not exist.`, // Create directories if not exist configDir := path.Dir(configFile) if _, err := os.Stat(configDir); err != nil { - os.MkdirAll(configDir, 0775) + os.MkdirAll(configDir, 0o775) } // Separate alias key and command at `--` or set the command to the last argument @@ -40,8 +40,8 @@ It will create the config directory if at does not exist.`, sep = len(args) - 1 } - c := config.GetFromFiles(configFile) - if err := c.SetAlias(args[:sep], args[sep:]); err != nil { + config.LoadFiles(configFile) + if err := config.SetAlias(args[:sep], args[sep:]); err != nil { fmt.Println("na: add:", err) os.Exit(1) } diff --git a/cmd/list.go b/cmd/list.go index 23e4177..fccdd61 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -5,6 +5,7 @@ import ( "os" "slices" + "github.com/moyiz/na/internal/cli" "github.com/moyiz/na/internal/config" "github.com/moyiz/na/internal/consts" "github.com/spf13/cobra" @@ -28,8 +29,8 @@ func validListArgs(cmd *cobra.Command, args []string, toComplete string) ([]stri if slices.Contains(os.Args, "--") { return []string{}, cobra.ShellCompDirectiveDefault } - config.GetFromFiles(AllConfigFiles()...) - return config.ListNextParts(args), cobra.ShellCompDirectiveNoFileComp + config.LoadFiles(AllConfigFiles()...) + return cli.ListNextParts(config.ListAliases(), args), cobra.ShellCompDirectiveNoFileComp } func listRun(cmd *cobra.Command, args []string) { diff --git a/cmd/remove.go b/cmd/remove.go index 8cb990f..26f9736 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -6,6 +6,7 @@ import ( "slices" "strings" + "github.com/moyiz/na/internal/cli" "github.com/moyiz/na/internal/config" "github.com/moyiz/na/internal/consts" "github.com/spf13/cobra" @@ -23,8 +24,8 @@ By default, the global (home directory config) configuration is used.`, Args: cobra.MinimumNArgs(1), ValidArgsFunction: validRemoveArgs, Run: func(cmd *cobra.Command, args []string) { - c := config.GetFromFiles(ActiveConfigFile()) - if err := c.UnsetAlias(args...); err != nil { + config.LoadFiles(ActiveConfigFile()) + if err := config.UnsetAlias(args...); err != nil { fmt.Println("na:", strings.Join(args, " ")+":", err) os.Exit(1) } @@ -35,6 +36,6 @@ func validRemoveArgs(cmd *cobra.Command, args []string, toComplete string) ([]st if slices.Contains(os.Args, "--") { return []string{}, cobra.ShellCompDirectiveDefault } - config.GetFromFiles(ActiveConfigFile()) - return config.ListNextParts(args), cobra.ShellCompDirectiveNoFileComp + config.LoadFiles(ActiveConfigFile()) + return cli.ListNextParts(config.ListAliases(), args), cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/root.go b/cmd/root.go index 7f32ec0..60931a3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -53,7 +53,7 @@ func AllConfigFiles() []string { func init() { cobra.EnableCommandSorting = true - cobra.OnInitialize(func() { config.GetFromFiles(AllConfigFiles()...) }) + cobra.OnInitialize(func() { config.LoadFiles(AllConfigFiles()...) }) rootCmd.PersistentFlags().StringP("config", "c", "", "Path of the config file to use") rootCmd.PersistentFlags().BoolP("local", "l", false, "Use local config (.na.yaml)") rootCmd.MarkFlagsMutuallyExclusive("local", "config") diff --git a/cmd/run.go b/cmd/run.go index 9e0f3b2..a8af4ca 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -6,6 +6,7 @@ import ( "slices" "strings" + "github.com/moyiz/na/internal/cli" "github.com/moyiz/na/internal/config" "github.com/moyiz/na/internal/consts" "github.com/moyiz/na/internal/utils" @@ -34,8 +35,8 @@ func validRunArgs(cmd *cobra.Command, args []string, toComplete string) ([]strin // Potential location to auto complete commands return []string{}, cobra.ShellCompDirectiveDefault } - config.GetFromFiles(AllConfigFiles()...) - return config.ListNextParts(args), cobra.ShellCompDirectiveNoFileComp + config.LoadFiles(AllConfigFiles()...) + return cli.ListNextParts(config.ListAliases(), args), cobra.ShellCompDirectiveNoFileComp } func runRun(cmd *cobra.Command, args []string) { @@ -52,8 +53,8 @@ func runRun(cmd *cobra.Command, args []string) { aliasParts = args } - c := config.GetFromFiles(AllConfigFiles()...) - if alias, err := c.GetAlias(aliasParts...); err != nil { + config.LoadFiles(AllConfigFiles()...) + if alias, err := config.GetAlias(aliasParts...); err != nil { fmt.Println("na:", strings.Join(aliasParts, " ")+":", err) } else { utils.RunInCurrentShell(alias.Command, extraArgs) diff --git a/internal/cli/completion.go b/internal/cli/completion.go new file mode 100644 index 0000000..e973148 --- /dev/null +++ b/internal/cli/completion.go @@ -0,0 +1,31 @@ +package cli + +import ( + "slices" + "strings" + + "github.com/moyiz/na/internal/config" +) + +// Given a list of alias name parts, return a list of valid next parts. +// Example: +// +// my: +// aliases: +// one: cmd1 +// two: cmd2 +// +// ListNextParts([]string{"my"}) -> []string{"aliases"} +// ListNextParts([]string{"my", "aliases"}) -> []string{"cmd1", "cmd2"} +func ListNextParts(aliases []config.Alias, parts []string) []string { + currentPrefix := strings.Join(parts, " ") + suggestions := make([]string, 0) + for _, a := range aliases { + trail, found := strings.CutPrefix(a.Name, currentPrefix) + if tf := strings.Fields(trail); found && len(tf) > 0 && !slices.Contains(suggestions, tf[0]) { + suggestions = append(suggestions, tf[0]) + } + } + + return suggestions +} diff --git a/internal/config/config.go b/internal/config/config.go index 1cfb96b..407f383 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,7 +3,6 @@ package config import ( "bytes" "encoding/json" - "errors" "path" "sort" "strings" @@ -11,65 +10,80 @@ import ( "github.com/spf13/viper" ) -func GetFromFiles(filePath ...string) *Config { +type Config struct { + v *viper.Viper +} + +var c Config + +func init() { + c = Config{v: viper.New()} +} + +type Alias struct { + Name string + Command string +} + +func LoadFiles(filePath ...string) map[string]any { + return c.LoadFiles(filePath...) +} + +func (c *Config) LoadFiles(filePath ...string) map[string]any { for i, p := range filePath { - viper.SetConfigFile(path.Clean(p)) + c.v.SetConfigFile(path.Clean(p)) if i == 0 { - viper.ReadInConfig() + c.v.ReadInConfig() } else { - viper.MergeInConfig() + c.v.MergeInConfig() } } - return &Config{aliases: viper.AllSettings()} + return c.v.AllSettings() } -type Config struct { - aliases map[string]interface{} +func SetAlias(key []string, command []string) error { + return c.SetAlias(key, command) } -func (c *Config) Write() error { - encoded, _ := json.MarshalIndent(c.aliases, "", " ") - viper.ReadConfig(bytes.NewReader(encoded)) - return viper.WriteConfig() +func (c *Config) SetAlias(name []string, command []string) error { + c.v.Set(strings.Join(name, "."), strings.Join(command, " ")) + return c.v.WriteConfig() } -func (c *Config) SetAlias(key []string, command []string) error { - viper.Set(strings.Join(key, "."), strings.Join(command, " ")) - return viper.WriteConfig() +func UnsetAlias(key ...string) error { + return c.UnsetAlias(key...) } func (c *Config) UnsetAlias(key ...string) error { var parent map[string]interface{} var keyIsMap, keyExists bool keySize := len(key) - aliasPointer := c.aliases + settings := c.v.AllSettings() + aliasWalker := settings for i, k := range key { - parent = aliasPointer - _, keyExists = aliasPointer[k] - aliasPointer, keyIsMap = aliasPointer[k].(map[string]interface{}) + parent = aliasWalker + _, keyExists = aliasWalker[k] + aliasWalker, keyIsMap = aliasWalker[k].(map[string]interface{}) if !keyExists { - return errors.New("not found") + return ErrAliasNotFound } else if !keyIsMap && i < keySize-1 { - return errors.New("key is invalid. Did you mean `" + strings.Join(key[:i+1], " ") + "`?") + return ErrInvalidAliasKey{strings.Join(key[:i+1], " ")} } else if i == keySize-1 { break } } delete(parent, key[keySize-1]) - return c.Write() + encoded, _ := json.MarshalIndent(settings, "", " ") + c.v.ReadConfig(bytes.NewReader(encoded)) + return c.v.WriteConfig() } -type Alias struct { - Name string - Command string +func ListAliases(prefix ...string) []Alias { + return c.ListAliases(prefix...) } func (c *Config) ListAliases(prefix ...string) []Alias { - return ListAliases(prefix...) -} - -func ListAliases(prefix ...string) []Alias { - keys := viper.AllKeys() + keys := c.v.AllKeys() sort.Strings(keys) aliases := make([]Alias, 0) @@ -78,7 +92,7 @@ func ListAliases(prefix ...string) []Alias { if strings.HasPrefix(k, aliasPrefix) { aliases = append(aliases, Alias{ Name: strings.ReplaceAll(k, ".", " "), - Command: viper.GetString(k), + Command: c.v.GetString(k), }) } } @@ -86,35 +100,15 @@ func ListAliases(prefix ...string) []Alias { return aliases } +func GetAlias(part ...string) (Alias, error) { + return c.GetAlias(part...) +} + func (c *Config) GetAlias(part ...string) (Alias, error) { key := strings.Join(part, ".") - if command := viper.GetString(key); command == "" { - return Alias{}, errors.New("not found") + if command := c.v.GetString(key); command == "" { + return Alias{}, ErrAliasNotFound } else { return Alias{Name: strings.ReplaceAll(key, ".", " "), Command: command}, nil } } - -// Given a list of alias name parts, return a list of valid next parts. -// Example: -// -// my: -// aliases: -// one: cmd1 -// two: cmd2 -// -// ListNextParts([]string{"my"}) -> []string{"aliases"} -// ListNextParts([]string{"my", "aliases"}) -> []string{"cmd1", "cmd2"} -func ListNextParts(parts []string) []string { - currentPrefix := strings.Join(parts, " ") - suggestions := make([]string, 0) - for _, a := range ListAliases(parts...) { - trail, _ := strings.CutPrefix(a.Name, currentPrefix) - if trailFields := strings.Fields(trail); len(trailFields) > 0 { - suggestions = append(suggestions, trailFields[0]) - } else { - break - } - } - return suggestions -} diff --git a/internal/config/errors.go b/internal/config/errors.go new file mode 100644 index 0000000..144ca1b --- /dev/null +++ b/internal/config/errors.go @@ -0,0 +1,16 @@ +package config + +import ( + "errors" + "fmt" +) + +var ErrAliasNotFound = errors.New("not found") + +type ErrInvalidAliasKey struct { + value string +} + +func (e ErrInvalidAliasKey) Error() string { + return fmt.Sprintf("Key is invalid. Did you mean `" + e.value + "`?") +}