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 it to the config file Viper is using. Only the // file's own settings are written back, not defaults or environment // variables. It returns the stored value (as ValueOf gives it) and its kind. func StoreValue( conf *JobSearchConfig, key string, value string, ) (any, reflect.Kind, error) { return storeValue(conf, key, value) } // storeValue is StoreValue for any section, so key types that // JobSearchConfig doesn't have yet can be tested. func storeValue(s Section, key string, value string) (any, reflect.Kind, error) { confType, err := s.TypeOf(key) if err != nil { return nil, confType, err } // Check the value is of the right kind first, for a clearer error than // SetValue gives. if _, err := parseValue(confType, value); err != nil { return nil, confType, fmt.Errorf( "config value type not accepted, expected <%s>", confType, ) } // SetValue parses the string into the field's own type (e.g. int8, // float32 or time.Duration) and rejects values the configuration itself // would not accept, e.g. an unknown log level or an unparseable duration. if err := s.SetValue(key, value); err != nil { return nil, confType, err } confValue, err := s.ValueOf(key) if err != nil { return nil, confType, err } viper.Set(key, confValue) file, err := readUsedConfigFile() if err != nil { return nil, confType, err } file.Set(key, confValue) if err := file.WriteConfig(); err != nil { return nil, confType, err } return confValue, confType, nil } // parseValue parses value as a value of kind, returning // errors.ErrUnsupported for a kind it can't parse. func parseValue(kind reflect.Kind, value string) (any, error) { switch kind { case reflect.Bool: return strconv.ParseBool(value) case reflect.String: return value, nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.ParseInt(value, 10, 64) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return strconv.ParseUint(value, 10, 64) case reflect.Float32, reflect.Float64: return strconv.ParseFloat(value, 64) } return nil, errors.ErrUnsupported } // PurgeValue rewrites the config file Viper is using without key. The // file's other settings are kept as they are; defaults and environment // variables are not written to it. func PurgeValue(conf *JobSearchConfig, key string) error { if _, err := conf.TypeOf(key); err != nil { return err } file, err := readUsedConfigFile() if err != nil { return err } out := viper.New() out.SetConfigFile(file.ConfigFileUsed()) for _, k := range file.AllKeys() { if k != key { out.Set(k, file.Get(k)) } } return out.WriteConfig() } // readUsedConfigFile reads the config file Viper is using into a new Viper, so // it holds only the file's settings. func readUsedConfigFile() (*viper.Viper, error) { path := viper.ConfigFileUsed() if path == "" { return nil, errors.New("no configuration file in use") } file := viper.New() file.SetConfigFile(path) if err := file.ReadInConfig(); err != nil { return nil, err } return file, nil }