From 44de214988b0b674ab8ae2c9cbd08126fe5395e0 Mon Sep 17 00:00:00 2001 From: Fabian 'xx4h' Sylvester Date: Thu, 17 Oct 2024 14:34:04 +0200 Subject: [PATCH] feat: extend & improve config subcommand * validate inputs * add remove subcommand --- cmd/config.go | 1 + cmd/config_rem.go | 55 ++++++++++++ cmd/config_set.go | 5 +- pkg/config/config.go | 34 ++++++++ pkg/config/validate.go | 186 +++++++++++++++++++++++++++++++++++++++++ pkg/hctl.go | 10 +++ 6 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 cmd/config_rem.go create mode 100644 pkg/config/validate.go diff --git a/cmd/config.go b/cmd/config.go index 54668a4..3a95c3c 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -34,6 +34,7 @@ func newConfigCmd(h *pkg.Hctl, out io.Writer) *cobra.Command { cmd.AddCommand( newConfigGetCmd(h, out), newConfigSetCmd(h, out), + newConfigRemCmd(h, out), ) return cmd diff --git a/cmd/config_rem.go b/cmd/config_rem.go new file mode 100644 index 0000000..a8bf3ff --- /dev/null +++ b/cmd/config_rem.go @@ -0,0 +1,55 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package cmd + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "github.com/xx4h/hctl/pkg" + o "github.com/xx4h/hctl/pkg/output" +) + +const ( + // editorconfig-checker-disable + configRemExample = ` + # Remove config option + hctl config rem device_map.a + hctl config remove device_map.b + ` + // editorconfig-checker-enable +) + +func newConfigRemCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove PATH", + Short: "Set config variables", + Aliases: []string{"r", "re", "rem", "remo"}, + Example: configRemExample, + Args: cobra.MatchAll(cobra.ExactArgs(1)), + ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compListConfig(toComplete, args, h) + }, + Run: func(_ *cobra.Command, args []string) { + if err := h.RemoveConfigOptionWrite(args[0]); err != nil { + o.PrintError(err) + } + o.PrintSuccess(fmt.Sprintf("Option `%s` successfully removed.", args[0])) + }, + } + + return cmd +} diff --git a/cmd/config_set.go b/cmd/config_set.go index f3f8fcd..7f9dd9c 100644 --- a/cmd/config_set.go +++ b/cmd/config_set.go @@ -15,6 +15,7 @@ package cmd import ( + "fmt" "io" "github.com/spf13/cobra" @@ -41,7 +42,7 @@ func newConfigSetCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { Args: cobra.MatchAll(cobra.ExactArgs(2)), ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 0 { - return noMoreArgsComp() + return nil, cobra.ShellCompDirectiveDefault } return compListConfig(toComplete, args, h) }, @@ -49,7 +50,7 @@ func newConfigSetCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { if err := h.SetConfigValueWrite(args[0], args[1]); err != nil { o.PrintError(err) } - o.PrintSuccess(args[0]) + o.PrintSuccess(fmt.Sprintf("Option `%s` successfully set to `%s`.", args[0], args[1])) }, } diff --git a/pkg/config/config.go b/pkg/config/config.go index ec98c15..38e8cc6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -22,6 +22,7 @@ import ( "path" "path/filepath" "reflect" + "slices" "strconv" "strings" @@ -258,6 +259,17 @@ func (c *Config) GetValueByPath(p string) (string, error) { } } +// Same as SetValueByPath, but also writes to config file +func (c *Config) RemoveOptionByPathWrite(p string) error { + if err := c.RemoveOptionByPath(p); err != nil { + return err + } + if err := c.WriteConfig(); err != nil { + return err + } + return nil +} + // Same as SetValueByPath, but also writes to config file func (c *Config) SetValueByPathWrite(p string, val any) error { if err := c.SetValueByPath(p, val); err != nil { @@ -290,10 +302,32 @@ func (c *Config) WriteConfig() error { return nil } +func (c *Config) RemoveOptionByPath(p string) error { + log.Info().Msgf("Removing option `%s`", p) + dynamicStringMap := []string{"device_map", "media_map"} + s := strings.Split(p, ".") + if len(s) == 2 && slices.Contains(dynamicStringMap, s[0]) { + m := c.Viper.GetStringMapString(s[0]) + delete(m, s[1]) + c.Viper.Set(s[0], m) + } + return fmt.Errorf("Deleting `%s` is currently not supported, use set instead", s[1]) +} + func (c *Config) SetValueByPath(p string, val any) error { + if err := validateSet(p, val); err != nil { + return err + } // set config element by path p and value v log.Info().Msgf("Setting `%v` to `%v`", p, val) + dynamicStringMap := []string{"device_map", "media_map"} s := strings.Split(p, ".") + if len(s) == 2 && slices.Contains(dynamicStringMap, s[0]) { + m := c.Viper.GetStringMapString(s[0]) + m[s[1]] = val.(string) + c.Viper.Set(s[0], m) + return nil + } v, _, err := c.getElement(s) if err != nil { return err diff --git a/pkg/config/validate.go b/pkg/config/validate.go new file mode 100644 index 0000000..a21e6e5 --- /dev/null +++ b/pkg/config/validate.go @@ -0,0 +1,186 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "fmt" + "net" + "strconv" + "strings" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func validateIsSection(p []string) error { + if len(p) == 1 { + return fmt.Errorf("Cannot set value for section: %s", p[0]) + } + return nil +} + +func validateNonEmptyKey(p []string) error { + if p[len(p)-1] == "" { + return fmt.Errorf("Cannot use empty key in %s", p[0]) + } + return nil +} + +func validateLoggingString(value string) error { + _, err := zerolog.ParseLevel(value) + if err != nil { + return fmt.Errorf("Unknown log_level: %s (Supported: trace, debug, error, warn, info)", value) + } + return nil +} + +func validateSetMediaMap(path []string, value any) error { + log.Debug().Caller().Msgf("Validating set for %s: %+v", path, value) + s, ok := value.(string) + if !ok { + return fmt.Errorf("media_map value needs to be string") + } + if strings.HasPrefix(s, "~") { + return fmt.Errorf("media_map does not support tilde path expansion yet") + } + return nil +} + +func validateSetDeviceMap(path []string, value any) error { + log.Debug().Caller().Msgf("Validating set for %s: %+v", path, value) + _, ok := value.(string) + if !ok { + return fmt.Errorf("device_map value needs to be string") + } + return nil +} + +func validateSetLogging(path []string, value any) error { + log.Debug().Caller().Msgf("Validating set for %s: %+v", path, value) + s, ok := value.(string) + if !ok { + return fmt.Errorf("device_map value needs to be string") + } + opt := path[len(path)-1] + if opt == "log_level" { + return validateLoggingString(s) + } + return fmt.Errorf("unknown config option for logging: %s", path[len(path)-1]) +} + +func validateSetHub(path []string, value any) error { + log.Debug().Caller().Msgf("Validating set for %s: %+v", path, value) + opt := path[len(path)-1] + switch opt { + case "type": + if value != "hass" { + return fmt.Errorf("Unknown hub type: %s (Supported: hass)", value) + } + case "url": + case "token": + default: + return fmt.Errorf("Unknown config option for hub: %s", opt) + } + return nil +} + +func validateSetHandling(path []string, value any) error { + log.Debug().Caller().Msgf("Validating set for %s: %+v", path, value) + opt := path[len(path)-1] + switch opt { + case "fuzz": + s := value.(string) + if _, err := strconv.ParseBool(s); err != nil { + return fmt.Errorf("Handling fuzz needs to be true/false") + } + default: + return fmt.Errorf("Unknown config option for handling: %s", opt) + } + return nil +} + +func validateSetCompletion(path []string, value any) error { + log.Debug().Caller().Msgf("Validating set for %s: %+v", path, value) + opt := path[len(path)-1] + switch opt { + case "short_names": + s := value.(string) + if _, err := strconv.ParseBool(s); err != nil { + return fmt.Errorf("Completion short_names needs to be true/false") + } + default: + return fmt.Errorf("Unknown config option for completion: %s", opt) + } + return nil +} + +func validateSetServe(path []string, value any) error { + log.Debug().Caller().Msgf("Validating set for %s: %+v", path, value) + opt := path[len(path)-1] + switch opt { + case "ip": + s, ok := value.(string) + if !ok { + return fmt.Errorf("device_map value needs to be string") + } + if ip := net.ParseIP(s); ip == nil { + return fmt.Errorf("Serve ip option need valid ip address") + } + case "port": + s := value.(string) + port, err := strconv.Atoi(s) + if err != nil { + return fmt.Errorf("Serve port needs to be a number") + } + if port < 1024 { + return fmt.Errorf("Use a non-well-known port (>1023)") + } + if port > 65535 { + return fmt.Errorf("Usen a valid port in the range 1024-65535") + } + default: + return fmt.Errorf("Unknown config option for serve: %s", opt) + } + return nil +} + +func validateSet(path string, value any) error { + p := strings.Split(path, ".") + if err := validateIsSection(p); err != nil { + return err + } + if err := validateNonEmptyKey(p); err != nil { + return err + } + + switch p[0] { + case "media_map": + return validateSetMediaMap(p, value) + case "device_map": + return validateSetDeviceMap(p, value) + case "logging": + return validateSetLogging(p, value) + case "hub": + return validateSetHub(p, value) + case "handling": + return validateSetHandling(p, value) + case "completion": + return validateSetCompletion(p, value) + case "serve": + return validateSetServe(p, value) + default: + return fmt.Errorf("unknown config option: %s", path) + } +} diff --git a/pkg/hctl.go b/pkg/hctl.go index 34599d2..a2e2358 100644 --- a/pkg/hctl.go +++ b/pkg/hctl.go @@ -84,6 +84,16 @@ func (h *Hctl) GetMap(k string) map[string]string { return h.cfg.Viper.GetStringMapString(k) } +func (h *Hctl) RemoveConfigOption(p string) error { + err := h.cfg.RemoveOptionByPath(p) + return err +} + +func (h *Hctl) RemoveConfigOptionWrite(p string) error { + err := h.cfg.RemoveOptionByPathWrite(p) + return err +} + func (h *Hctl) SetConfigValueWrite(p string, v string) error { err := h.cfg.SetValueByPathWrite(p, v) return err