diff --git a/internal/cli/config/set.go b/internal/cli/config/set.go index fd88f17..3a9dafc 100644 --- a/internal/cli/config/set.go +++ b/internal/cli/config/set.go @@ -4,8 +4,6 @@ import ( "errors" "fmt" "log/slog" - "reflect" - "strconv" "git.avsdev.uk/Go/job-search/internal/cli" "git.avsdev.uk/Go/job-search/internal/config" @@ -13,65 +11,6 @@ import ( "github.com/spf13/viper" ) -func setConfig(conf *config.JobSearchConfig, key string, value string) error { - var confValue any = nil - var confType reflect.Kind - var err error - - confType, err = conf.TypeOf(key) - if err != nil { - return err - } - - switch confType { - case reflect.Bool: - confValue, err = strconv.ParseBool(value) - case reflect.String: - confValue = value - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - confValue, err = strconv.ParseInt(value, 10, 64) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - confValue, err = strconv.ParseUint(value, 10, 64) - case reflect.Float32, reflect.Float64: - confValue, err = strconv.ParseFloat(value, 64) - default: - err = errors.ErrUnsupported - } - if err != nil { - return fmt.Errorf("config value type not accepted, expected <%s>", confType) - } - - viper.Set(key, confValue) - if err := viper.WriteConfig(); err != nil { - return err - } - fmt.Print(key, " = ", confValue, " <", confType, ">\n") - - return nil -} -func unsetConfig(conf *config.JobSearchConfig, key string) error { - if _, err := conf.TypeOf(key); err != nil { - return err - } - - v := viper.New() - v.SetConfigFile(viper.ConfigFileUsed()) - - keys := viper.AllKeys() - for i := range keys { - k := keys[i] - if k != key { - v.Set(k, viper.Get(k)) - } - } - - if err := v.WriteConfig(); err != nil { - return err - } - - return nil -} - func commandSet(opts *cli.Options) *cobra.Command { cmd := &cobra.Command{ Use: "set [config_key] [config_value]", @@ -111,10 +50,17 @@ func commandSet(opts *cli.Options) *cobra.Command { } if unset { - return unsetConfig(conf, args[0]) - } else { - return setConfig(conf, args[0], args[1]) + return config.PurgeValue(conf, args[0]) } + + confValue, confType, err := config.StoreValue(conf, args[0], args[1]) + if err != nil { + return err + } + + fmt.Print(args[0], " = ", confValue, " <", confType, ">\n") + + return nil }, } cmd.Flags().BoolP("unset", "x", false, "clear configuration setting") diff --git a/internal/config/config.go b/internal/config/config.go index aa5321c..1814b19 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -54,16 +54,20 @@ func LoadFromViper(conf *JobSearchConfig) { if keyType == reflect.Invalid { continue } + var err error switch keyType { case reflect.String: - conf.SetValue(key, viper.GetString(key)) + err = conf.SetValue(key, viper.GetString(key)) case reflect.Int: - conf.SetValue(key, viper.GetInt(key)) + err = conf.SetValue(key, viper.GetInt(key)) case reflect.Bool: - conf.SetValue(key, viper.GetBool(key)) + err = conf.SetValue(key, viper.GetBool(key)) default: slog.Error(fmt.Sprintf("unknown key type: %s", keyType)) } + if err != nil { + slog.Warn("ignoring invalid config value", "key", key, "err", err) + } } } diff --git a/internal/config/load.go b/internal/config/load.go index 17c5515..ccda30a 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -3,6 +3,7 @@ package config import ( "fmt" "log/slog" + "strings" "github.com/joho/godotenv" "github.com/spf13/viper" @@ -33,6 +34,7 @@ func loadConfig(cfgFile string) error { LoadDefaultConfig() viper.SetEnvPrefix(EnvPrefix) + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() // read in environment variables that match err := viper.ReadInConfig() diff --git a/internal/config/modify.go b/internal/config/modify.go new file mode 100644 index 0000000..7ed9499 --- /dev/null +++ b/internal/config/modify.go @@ -0,0 +1,83 @@ +package config + +import ( + "errors" + "fmt" + "reflect" + "strconv" + + "github.com/spf13/viper" +) + +// StoreValue parses value as the type of key, checks conf accepts it, sets it +// in conf and Viper and writes the config file Viper is using. It returns the +// parsed value and its kind. +func StoreValue( + conf *JobSearchConfig, key string, value string, +) (any, reflect.Kind, error) { + var confValue any = nil + var confType reflect.Kind + var err error + + confType, err = conf.TypeOf(key) + if err != nil { + return nil, confType, err + } + + switch confType { + case reflect.Bool: + confValue, err = strconv.ParseBool(value) + case reflect.String: + confValue = value + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + confValue, err = strconv.ParseInt(value, 10, 64) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + confValue, err = strconv.ParseUint(value, 10, 64) + case reflect.Float32, reflect.Float64: + confValue, err = strconv.ParseFloat(value, 64) + default: + err = errors.ErrUnsupported + } + if err != nil { + return nil, confType, fmt.Errorf( + "config value type not accepted, expected <%s>", confType, + ) + } + + // Reject values the configuration itself would not accept, e.g. an + // unknown log level or an unparseable duration. + if err := conf.SetValue(key, confValue); err != nil { + return nil, confType, err + } + + viper.Set(key, confValue) + if err := viper.WriteConfig(); err != nil { + return nil, confType, err + } + + return confValue, confType, nil +} + +// PurgeValue rewrites the config file Viper is using without key. +func PurgeValue(conf *JobSearchConfig, key string) error { + if _, err := conf.TypeOf(key); err != nil { + return err + } + + v := viper.New() + v.SetConfigFile(viper.ConfigFileUsed()) + + keys := viper.AllKeys() + for i := range keys { + k := keys[i] + if k != key { + v.Set(k, viper.Get(k)) + } + } + + if err := v.WriteConfig(); err != nil { + return err + } + + return nil +}