Skip to content

Commit

Permalink
feat: extend & improve config subcommand (#59)
Browse files Browse the repository at this point in the history
* validate inputs
* add remove subcommand
  • Loading branch information
xx4h authored Oct 17, 2024
1 parent 8b4f4c1 commit db1613a
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 2 deletions.
1 change: 1 addition & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions cmd/config_rem.go
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 3 additions & 2 deletions cmd/config_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package cmd

import (
"fmt"
"io"

"github.com/spf13/cobra"
Expand All @@ -41,15 +42,15 @@ 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)
},
Run: func(_ *cobra.Command, args []string) {
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]))
},
}

Expand Down
34 changes: 34 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"path"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
186 changes: 186 additions & 0 deletions pkg/config/validate.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
10 changes: 10 additions & 0 deletions pkg/hctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit db1613a

Please sign in to comment.