diff --git a/config/expertise.go b/config/expertise.go index 9f515d4..277758b 100644 --- a/config/expertise.go +++ b/config/expertise.go @@ -5,6 +5,8 @@ package config import ( "fmt" "sync/atomic" + + "github.com/tevino/abool" ) // Expertise Level constants @@ -21,7 +23,9 @@ const ( ) var ( - expertiseLevel *int32 + expertiseLevel *int32 + expertiseLevelOption *Option + expertiseLevelOptionFlag = abool.New() ) func init() { @@ -32,7 +36,7 @@ func init() { } func registerExpertiseLevelOption() { - err := Register(&Option{ + expertiseLevelOption = &Option{ Name: "Expertise Level", Key: expertiseLevelKey, Description: "The Expertise Level controls the perceived complexity. Higher settings will show you more complex settings and information. This might also affect various other things relying on this setting. Modified settings in higher expertise levels stay in effect when switching back. (Unlike the Release Level)", @@ -46,15 +50,31 @@ func registerExpertiseLevelOption() { ExternalOptType: "string list", ValidationRegex: fmt.Sprintf("^(%s|%s|%s)$", ExpertiseLevelNameUser, ExpertiseLevelNameExpert, ExpertiseLevelNameDeveloper), - }) + } + + err := Register(expertiseLevelOption) if err != nil { panic(err) } + + expertiseLevelOptionFlag.Set() } func updateExpertiseLevel() { - new := findStringValue(expertiseLevelKey, "") - switch new { + // check if already registered + if !expertiseLevelOptionFlag.IsSet() { + return + } + // get value + value := expertiseLevelOption.activeFallbackValue + if expertiseLevelOption.activeValue != nil { + value = expertiseLevelOption.activeValue + } + if expertiseLevelOption.activeDefaultValue != nil { + value = expertiseLevelOption.activeDefaultValue + } + // set atomic value + switch value.stringVal { case ExpertiseLevelNameUser: atomic.StoreInt32(expertiseLevel, int32(ExpertiseLevelUser)) case ExpertiseLevelNameExpert: diff --git a/config/get-safe.go b/config/get-safe.go index a8eaff8..08f8675 100644 --- a/config/get-safe.go +++ b/config/get-safe.go @@ -11,15 +11,25 @@ var ( // GetAsString returns a function that returns the wanted string with high performance. func (cs *safe) GetAsString(name string, fallback string) StringOption { - valid := getValidityFlag() - value := findStringValue(name, fallback) + valid := GetValidityFlag() + option, valueCache := getValueCache(name, nil, OptTypeString) + value := fallback + if valueCache != nil { + value = valueCache.stringVal + } var lock sync.Mutex + return func() string { lock.Lock() defer lock.Unlock() if !valid.IsSet() { - valid = getValidityFlag() - value = findStringValue(name, fallback) + valid = GetValidityFlag() + option, valueCache = getValueCache(name, option, OptTypeString) + if valueCache != nil { + value = valueCache.stringVal + } else { + value = fallback + } } return value } @@ -27,15 +37,25 @@ func (cs *safe) GetAsString(name string, fallback string) StringOption { // GetAsStringArray returns a function that returns the wanted string with high performance. func (cs *safe) GetAsStringArray(name string, fallback []string) StringArrayOption { - valid := getValidityFlag() - value := findStringArrayValue(name, fallback) + valid := GetValidityFlag() + option, valueCache := getValueCache(name, nil, OptTypeStringArray) + value := fallback + if valueCache != nil { + value = valueCache.stringArrayVal + } var lock sync.Mutex + return func() []string { lock.Lock() defer lock.Unlock() if !valid.IsSet() { - valid = getValidityFlag() - value = findStringArrayValue(name, fallback) + valid = GetValidityFlag() + option, valueCache = getValueCache(name, option, OptTypeStringArray) + if valueCache != nil { + value = valueCache.stringArrayVal + } else { + value = fallback + } } return value } @@ -43,15 +63,25 @@ func (cs *safe) GetAsStringArray(name string, fallback []string) StringArrayOpti // GetAsInt returns a function that returns the wanted int with high performance. func (cs *safe) GetAsInt(name string, fallback int64) IntOption { - valid := getValidityFlag() - value := findIntValue(name, fallback) + valid := GetValidityFlag() + option, valueCache := getValueCache(name, nil, OptTypeInt) + value := fallback + if valueCache != nil { + value = valueCache.intVal + } var lock sync.Mutex + return func() int64 { lock.Lock() defer lock.Unlock() if !valid.IsSet() { - valid = getValidityFlag() - value = findIntValue(name, fallback) + valid = GetValidityFlag() + option, valueCache = getValueCache(name, option, OptTypeInt) + if valueCache != nil { + value = valueCache.intVal + } else { + value = fallback + } } return value } @@ -59,15 +89,25 @@ func (cs *safe) GetAsInt(name string, fallback int64) IntOption { // GetAsBool returns a function that returns the wanted int with high performance. func (cs *safe) GetAsBool(name string, fallback bool) BoolOption { - valid := getValidityFlag() - value := findBoolValue(name, fallback) + valid := GetValidityFlag() + option, valueCache := getValueCache(name, nil, OptTypeBool) + value := fallback + if valueCache != nil { + value = valueCache.boolVal + } var lock sync.Mutex + return func() bool { lock.Lock() defer lock.Unlock() if !valid.IsSet() { - valid = getValidityFlag() - value = findBoolValue(name, fallback) + valid = GetValidityFlag() + option, valueCache = getValueCache(name, option, OptTypeBool) + if valueCache != nil { + value = valueCache.boolVal + } else { + value = fallback + } } return value } diff --git a/config/get.go b/config/get.go index 7bc0501..df1cd08 100644 --- a/config/get.go +++ b/config/get.go @@ -15,14 +15,59 @@ type ( BoolOption func() bool ) +func getValueCache(name string, option *Option, requestedType uint8) (*Option, *valueCache) { + // get option + if option == nil { + var ok bool + optionsLock.RLock() + option, ok = options[name] + optionsLock.RUnlock() + if !ok { + log.Errorf("config: request for unregistered option: %s", name) + return nil, nil + } + } + + // check type + if requestedType != option.OptType { + log.Errorf("config: bad type: requested %s as %s, but is %s", name, getTypeName(requestedType), getTypeName(option.OptType)) + return option, nil + } + + // lock option + option.Lock() + defer option.Unlock() + + // check release level + if option.ReleaseLevel <= getReleaseLevel() && option.activeValue != nil { + return option, option.activeValue + } + + if option.activeDefaultValue != nil { + return option, option.activeDefaultValue + } + + return option, option.activeFallbackValue +} + // GetAsString returns a function that returns the wanted string with high performance. func GetAsString(name string, fallback string) StringOption { - valid := getValidityFlag() - value := findStringValue(name, fallback) + valid := GetValidityFlag() + option, valueCache := getValueCache(name, nil, OptTypeString) + value := fallback + if valueCache != nil { + value = valueCache.stringVal + } + return func() string { if !valid.IsSet() { - valid = getValidityFlag() - value = findStringValue(name, fallback) + valid = GetValidityFlag() + option, valueCache = getValueCache(name, option, OptTypeString) + if valueCache != nil { + value = valueCache.stringVal + } else { + value = fallback + } } return value } @@ -30,12 +75,22 @@ func GetAsString(name string, fallback string) StringOption { // GetAsStringArray returns a function that returns the wanted string with high performance. func GetAsStringArray(name string, fallback []string) StringArrayOption { - valid := getValidityFlag() - value := findStringArrayValue(name, fallback) + valid := GetValidityFlag() + option, valueCache := getValueCache(name, nil, OptTypeStringArray) + value := fallback + if valueCache != nil { + value = valueCache.stringArrayVal + } + return func() []string { if !valid.IsSet() { - valid = getValidityFlag() - value = findStringArrayValue(name, fallback) + valid = GetValidityFlag() + option, valueCache = getValueCache(name, option, OptTypeStringArray) + if valueCache != nil { + value = valueCache.stringArrayVal + } else { + value = fallback + } } return value } @@ -43,12 +98,22 @@ func GetAsStringArray(name string, fallback []string) StringArrayOption { // GetAsInt returns a function that returns the wanted int with high performance. func GetAsInt(name string, fallback int64) IntOption { - valid := getValidityFlag() - value := findIntValue(name, fallback) + valid := GetValidityFlag() + option, valueCache := getValueCache(name, nil, OptTypeInt) + value := fallback + if valueCache != nil { + value = valueCache.intVal + } + return func() int64 { if !valid.IsSet() { - valid = getValidityFlag() - value = findIntValue(name, fallback) + valid = GetValidityFlag() + option, valueCache = getValueCache(name, option, OptTypeInt) + if valueCache != nil { + value = valueCache.intVal + } else { + value = fallback + } } return value } @@ -56,19 +121,29 @@ func GetAsInt(name string, fallback int64) IntOption { // GetAsBool returns a function that returns the wanted int with high performance. func GetAsBool(name string, fallback bool) BoolOption { - valid := getValidityFlag() - value := findBoolValue(name, fallback) + valid := GetValidityFlag() + option, valueCache := getValueCache(name, nil, OptTypeBool) + value := fallback + if valueCache != nil { + value = valueCache.boolVal + } + return func() bool { if !valid.IsSet() { - valid = getValidityFlag() - value = findBoolValue(name, fallback) + valid = GetValidityFlag() + option, valueCache = getValueCache(name, option, OptTypeBool) + if valueCache != nil { + value = valueCache.boolVal + } else { + value = fallback + } } return value } } -// findValue find the correct value in the user or default config. -func findValue(key string) interface{} { +/* +func getAndFindValue(key string) interface{} { optionsLock.RLock() option, ok := options[key] optionsLock.RUnlock() @@ -77,6 +152,13 @@ func findValue(key string) interface{} { return nil } + return option.findValue() +} +*/ + +/* +// findValue finds the preferred value in the user or default config. +func (option *Option) findValue() interface{} { // lock option option.Lock() defer option.Unlock() @@ -91,88 +173,4 @@ func findValue(key string) interface{} { return option.DefaultValue } - -// findStringValue validates and returns the value with the given key. -func findStringValue(key string, fallback string) (value string) { - result := findValue(key) - if result == nil { - return fallback - } - v, ok := result.(string) - if ok { - return v - } - return fallback -} - -// findStringArrayValue validates and returns the value with the given key. -func findStringArrayValue(key string, fallback []string) (value []string) { - result := findValue(key) - if result == nil { - return fallback - } - - v, ok := result.([]interface{}) - if ok { - new := make([]string, len(v)) - for i, val := range v { - s, ok := val.(string) - if ok { - new[i] = s - } else { - return fallback - } - } - return new - } - - return fallback -} - -// findIntValue validates and returns the value with the given key. -func findIntValue(key string, fallback int64) (value int64) { - result := findValue(key) - if result == nil { - return fallback - } - switch v := result.(type) { - case int: - return int64(v) - case int8: - return int64(v) - case int16: - return int64(v) - case int32: - return int64(v) - case int64: - return v - case uint: - return int64(v) - case uint8: - return int64(v) - case uint16: - return int64(v) - case uint32: - return int64(v) - case uint64: - return int64(v) - case float32: - return int64(v) - case float64: - return int64(v) - } - return fallback -} - -// findBoolValue validates and returns the value with the given key. -func findBoolValue(key string, fallback bool) (value bool) { - result := findValue(key) - if result == nil { - return fallback - } - v, ok := result.(bool) - if ok { - return v - } - return fallback -} +*/ diff --git a/config/get_test.go b/config/get_test.go index 3028619..bef788a 100644 --- a/config/get_test.go +++ b/config/get_test.go @@ -1,6 +1,7 @@ package config import ( + "encoding/json" "testing" "github.com/safing/portbase/log" @@ -39,7 +40,7 @@ func quickRegister(t *testing.T, key string, optType uint8, defaultValue interfa } } -func TestGet(t *testing.T) { +func TestGet(t *testing.T) { //nolint:gocognit // reset options = make(map[string]*Option) @@ -48,41 +49,41 @@ func TestGet(t *testing.T) { t.Fatal(err) } - quickRegister(t, "monkey", OptTypeInt, -1) + quickRegister(t, "monkey", OptTypeString, "c") quickRegister(t, "zebras/zebra", OptTypeStringArray, []string{"a", "b"}) quickRegister(t, "elephant", OptTypeInt, -1) quickRegister(t, "hot", OptTypeBool, false) quickRegister(t, "cold", OptTypeBool, true) err = parseAndSetConfig(` - { - "monkey": "1", + { + "monkey": "a", "zebras": { "zebra": ["black", "white"] }, - "elephant": 2, + "elephant": 2, "hot": true, "cold": false - } - `) + } + `) if err != nil { t.Fatal(err) } err = parseAndSetDefaultConfig(` - { - "monkey": "0", - "snake": "0", - "elephant": 0 - } - `) + { + "monkey": "b", + "snake": "0", + "elephant": 0 + } + `) if err != nil { t.Fatal(err) } monkey := GetAsString("monkey", "none") - if monkey() != "1" { - t.Errorf("monkey should be 1, is %s", monkey()) + if monkey() != "a" { + t.Errorf("monkey should be a, is %s", monkey()) } zebra := GetAsStringArray("zebras/zebra", []string{}) @@ -106,10 +107,10 @@ func TestGet(t *testing.T) { } err = parseAndSetConfig(` - { - "monkey": "3" - } - `) + { + "monkey": "3" + } + `) if err != nil { t.Fatal(err) } @@ -131,6 +132,53 @@ func TestGet(t *testing.T) { GetAsInt("elephant", -1)() GetAsBool("hot", false)() + // perspective + + // load data + pLoaded := make(map[string]interface{}) + err = json.Unmarshal([]byte(`{ + "monkey": "a", + "zebras": { + "zebra": ["black", "white"] + }, + "elephant": 2, + "hot": true, + "cold": false + }`), &pLoaded) + if err != nil { + t.Fatal(err) + } + + // create + p, err := NewPerspective(pLoaded) + if err != nil { + t.Fatal(err) + } + + monkeyVal, ok := p.GetAsString("monkey") + if !ok || monkeyVal != "a" { + t.Errorf("[perspective] monkey should be a, is %+v", monkeyVal) + } + + zebraVal, ok := p.GetAsStringArray("zebras/zebra") + if !ok || len(zebraVal) != 2 || zebraVal[0] != "black" || zebraVal[1] != "white" { + t.Errorf("[perspective] zebra should be [\"black\", \"white\"], is %+v", zebraVal) + } + + elephantVal, ok := p.GetAsInt("elephant") + if !ok || elephantVal != 2 { + t.Errorf("[perspective] elephant should be 2, is %+v", elephantVal) + } + + hotVal, ok := p.GetAsBool("hot") + if !ok || !hotVal { + t.Errorf("[perspective] hot should be true, is %+v", hotVal) + } + + coldVal, ok := p.GetAsBool("cold") + if !ok || coldVal { + t.Errorf("[perspective] cold should be false, is %+v", coldVal) + } } func TestReleaseLevel(t *testing.T) { @@ -236,11 +284,9 @@ func BenchmarkGetAsStringCached(b *testing.B) { options = make(map[string]*Option) // Setup - err := parseAndSetConfig(` - { - "monkey": "banana" - } - `) + err := parseAndSetConfig(`{ + "monkey": "banana" + }`) if err != nil { b.Fatal(err) } @@ -257,11 +303,9 @@ func BenchmarkGetAsStringCached(b *testing.B) { func BenchmarkGetAsStringRefetch(b *testing.B) { // Setup - err := parseAndSetConfig(` - { - "monkey": "banana" - } - `) + err := parseAndSetConfig(`{ + "monkey": "banana" + }`) if err != nil { b.Fatal(err) } @@ -271,38 +315,34 @@ func BenchmarkGetAsStringRefetch(b *testing.B) { // Start benchmark for i := 0; i < b.N; i++ { - findStringValue("monkey", "no banana") + getValueCache("monkey", nil, OptTypeString) } } func BenchmarkGetAsIntCached(b *testing.B) { // Setup - err := parseAndSetConfig(` - { - "monkey": 1 - } - `) + err := parseAndSetConfig(`{ + "elephant": 1 + }`) if err != nil { b.Fatal(err) } - monkey := GetAsInt("monkey", -1) + elephant := GetAsInt("elephant", -1) // Reset timer for precise results b.ResetTimer() // Start benchmark for i := 0; i < b.N; i++ { - monkey() + elephant() } } func BenchmarkGetAsIntRefetch(b *testing.B) { // Setup - err := parseAndSetConfig(` - { - "monkey": 1 - } - `) + err := parseAndSetConfig(`{ + "elephant": 1 + }`) if err != nil { b.Fatal(err) } @@ -312,6 +352,6 @@ func BenchmarkGetAsIntRefetch(b *testing.B) { // Start benchmark for i := 0; i < b.N; i++ { - findIntValue("monkey", 1) + getValueCache("elephant", nil, OptTypeInt) } } diff --git a/config/option.go b/config/option.go index db33e3a..4d79560 100644 --- a/config/option.go +++ b/config/option.go @@ -41,6 +41,7 @@ type Option struct { Name string Key string // in path format: category/sub/key Description string + Help string OptType uint8 ExpertiseLevel uint8 @@ -52,9 +53,10 @@ type Option struct { ExternalOptType string ValidationRegex string - activeValue interface{} // runtime value (loaded from config file or set by user) - activeDefaultValue interface{} // runtime default value (may be set internally) - compiledRegex *regexp.Regexp + activeValue *valueCache // runtime value (loaded from config file or set by user) + activeDefaultValue *valueCache // runtime default value (may be set internally) + activeFallbackValue *valueCache // default value from option registration + compiledRegex *regexp.Regexp } // Export expors an option to a Record. diff --git a/config/perspective.go b/config/perspective.go new file mode 100644 index 0000000..e863449 --- /dev/null +++ b/config/perspective.go @@ -0,0 +1,128 @@ +package config + +import ( + "fmt" + + "github.com/safing/portbase/log" +) + +// Perspective is a view on configuration data without interfering with the configuration system. +type Perspective struct { + config map[string]*perspectiveOption +} + +type perspectiveOption struct { + option *Option + valueCache *valueCache +} + +// NewPerspective parses the given config and returns it as a new perspective. +func NewPerspective(config map[string]interface{}) (*Perspective, error) { + // flatten config structure + flatten(config, config, "") + + perspective := &Perspective{ + config: make(map[string]*perspectiveOption), + } + var firstErr error + var errCnt int + + optionsLock.Lock() +optionsLoop: + for key, option := range options { + // get option key from config + configValue, ok := config[key] + if !ok { + continue + } + // validate value + valueCache, err := validateValue(option, configValue) + if err != nil { + errCnt++ + if firstErr == nil { + firstErr = err + } + continue optionsLoop + } + + // add to perspective + perspective.config[key] = &perspectiveOption{ + option: option, + valueCache: valueCache, + } + } + optionsLock.Unlock() + + if firstErr != nil { + if errCnt > 0 { + return perspective, fmt.Errorf("encountered %d errors, first was: %s", errCnt, firstErr) + } + return perspective, firstErr + } + + return perspective, nil +} + +func (p *Perspective) getPerspectiveValueCache(name string, requestedType uint8) *valueCache { + // get option + pOption, ok := p.config[name] + if !ok { + // check if option exists at all + optionsLock.RLock() + _, ok = options[name] + optionsLock.RUnlock() + if !ok { + log.Errorf("config: request for unregistered option: %s", name) + } + return nil + } + + // check type + if requestedType != pOption.option.OptType { + log.Errorf("config: bad type: requested %s as %s, but is %s", name, getTypeName(requestedType), getTypeName(pOption.option.OptType)) + return nil + } + + // check release level + if pOption.option.ReleaseLevel > getReleaseLevel() { + return nil + } + + return pOption.valueCache +} + +// GetAsString returns a function that returns the wanted string with high performance. +func (p *Perspective) GetAsString(name string) (value string, ok bool) { + valueCache := p.getPerspectiveValueCache(name, OptTypeString) + if valueCache != nil { + return valueCache.stringVal, true + } + return "", false +} + +// GetAsStringArray returns a function that returns the wanted string with high performance. +func (p *Perspective) GetAsStringArray(name string) (value []string, ok bool) { + valueCache := p.getPerspectiveValueCache(name, OptTypeStringArray) + if valueCache != nil { + return valueCache.stringArrayVal, true + } + return nil, false +} + +// GetAsInt returns a function that returns the wanted int with high performance. +func (p *Perspective) GetAsInt(name string) (value int64, ok bool) { + valueCache := p.getPerspectiveValueCache(name, OptTypeInt) + if valueCache != nil { + return valueCache.intVal, true + } + return 0, false +} + +// GetAsBool returns a function that returns the wanted int with high performance. +func (p *Perspective) GetAsBool(name string) (value bool, ok bool) { + valueCache := p.getPerspectiveValueCache(name, OptTypeBool) + if valueCache != nil { + return valueCache.boolVal, true + } + return false, false +} diff --git a/config/registry.go b/config/registry.go index 0b2c221..11bfd76 100644 --- a/config/registry.go +++ b/config/registry.go @@ -26,14 +26,20 @@ func Register(option *Option) error { return fmt.Errorf("failed to register option: please set option.OptType") } + var err error + if option.ValidationRegex != "" { - var err error option.compiledRegex, err = regexp.Compile(option.ValidationRegex) if err != nil { return fmt.Errorf("config: could not compile option.ValidationRegex: %s", err) } } + option.activeFallbackValue, err = validateValue(option, option.DefaultValue) + if err != nil { + return fmt.Errorf("config: invalid default value: %s", err) + } + optionsLock.Lock() defer optionsLock.Unlock() options[option.Key] = option diff --git a/config/registry_test.go b/config/registry_test.go index 66fcb7f..3f5593a 100644 --- a/config/registry_test.go +++ b/config/registry_test.go @@ -15,7 +15,7 @@ func TestRegistry(t *testing.T) { ReleaseLevel: ReleaseLevelStable, ExpertiseLevel: ExpertiseLevelUser, OptType: OptTypeString, - DefaultValue: "default", + DefaultValue: "water", ValidationRegex: "^(banana|water)$", }); err != nil { t.Error(err) diff --git a/config/release.go b/config/release.go index 14b239c..f72f93e 100644 --- a/config/release.go +++ b/config/release.go @@ -5,6 +5,8 @@ package config import ( "fmt" "sync/atomic" + + "github.com/tevino/abool" ) // Release Level constants @@ -21,7 +23,9 @@ const ( ) var ( - releaseLevel *int32 + releaseLevel *int32 + releaseLevelOption *Option + releaseLevelOptionFlag = abool.New() ) func init() { @@ -32,7 +36,7 @@ func init() { } func registerReleaseLevelOption() { - err := Register(&Option{ + releaseLevelOption = &Option{ Name: "Release Level", Key: releaseLevelKey, Description: "The Release Level changes which features are available to you. Some beta or experimental features are also available in the stable release channel. Unavailable settings are set to the default value.", @@ -46,15 +50,31 @@ func registerReleaseLevelOption() { ExternalOptType: "string list", ValidationRegex: fmt.Sprintf("^(%s|%s|%s)$", ReleaseLevelNameStable, ReleaseLevelNameBeta, ReleaseLevelNameExperimental), - }) + } + + err := Register(releaseLevelOption) if err != nil { panic(err) } + + releaseLevelOptionFlag.Set() } func updateReleaseLevel() { - new := findStringValue(releaseLevelKey, "") - switch new { + // check if already registered + if !releaseLevelOptionFlag.IsSet() { + return + } + // get value + value := releaseLevelOption.activeFallbackValue + if releaseLevelOption.activeValue != nil { + value = releaseLevelOption.activeValue + } + if releaseLevelOption.activeDefaultValue != nil { + value = releaseLevelOption.activeDefaultValue + } + // set atomic value + switch value.stringVal { case ReleaseLevelNameStable: atomic.StoreInt32(releaseLevel, int32(ReleaseLevelStable)) case ReleaseLevelNameBeta: diff --git a/config/set.go b/config/set.go index 8e55714..a0f6789 100644 --- a/config/set.go +++ b/config/set.go @@ -19,7 +19,8 @@ var ( validityFlagLock sync.RWMutex ) -func getValidityFlag() *abool.AtomicBool { +// GetValidityFlag returns a flag that signifies if the configuration has been changed. This flag must not be changed, only read. +func GetValidityFlag() *abool.AtomicBool { validityFlagLock.RLock() defer validityFlagLock.RUnlock() return validityFlag @@ -41,14 +42,24 @@ func signalChanges() { // setConfig sets the (prioritized) user defined config. func setConfig(newValues map[string]interface{}) error { + var firstErr error + var errCnt int + optionsLock.Lock() for key, option := range options { newValue, ok := newValues[key] option.Lock() + option.activeValue = nil if ok { - option.activeValue = newValue - } else { - option.activeValue = nil + valueCache, err := validateValue(option, newValue) + if err == nil { + option.activeValue = valueCache + } else { + errCnt++ + if firstErr == nil { + firstErr = err + } + } } option.Unlock() } @@ -56,19 +67,37 @@ func setConfig(newValues map[string]interface{}) error { signalChanges() go pushFullUpdate() + + if firstErr != nil { + if errCnt > 0 { + return fmt.Errorf("encountered %d errors, first was: %s", errCnt, firstErr) + } + return firstErr + } + return nil } // SetDefaultConfig sets the (fallback) default config. func SetDefaultConfig(newValues map[string]interface{}) error { + var firstErr error + var errCnt int + optionsLock.Lock() for key, option := range options { newValue, ok := newValues[key] option.Lock() + option.activeDefaultValue = nil if ok { - option.activeDefaultValue = newValue - } else { - option.activeDefaultValue = nil + valueCache, err := validateValue(option, newValue) + if err == nil { + option.activeDefaultValue = valueCache + } else { + errCnt++ + if firstErr == nil { + firstErr = err + } + } } option.Unlock() } @@ -76,51 +105,15 @@ func SetDefaultConfig(newValues map[string]interface{}) error { signalChanges() go pushFullUpdate() - return nil -} -func validateValue(option *Option, value interface{}) error { - switch v := value.(type) { - case string: - if option.OptType != OptTypeString { - return fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, v) + if firstErr != nil { + if errCnt > 0 { + return fmt.Errorf("encountered %d errors, first was: %s", errCnt, firstErr) } - if option.compiledRegex != nil { - if !option.compiledRegex.MatchString(v) { - return fmt.Errorf("validation failed: string \"%s\" did not match regex for option %s", v, option.Key) - } - } - return nil - case []string: - if option.OptType != OptTypeStringArray { - return fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, v) - } - if option.compiledRegex != nil { - for pos, entry := range v { - if !option.compiledRegex.MatchString(entry) { - return fmt.Errorf("validation failed: string \"%s\" at index %d did not match regex for option %s", entry, pos, option.Key) - } - } - } - return nil - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - if option.OptType != OptTypeInt { - return fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, v) - } - if option.compiledRegex != nil { - if !option.compiledRegex.MatchString(fmt.Sprintf("%d", v)) { - return fmt.Errorf("validation failed: number \"%d\" did not match regex for option %s", v, option.Key) - } - } - return nil - case bool: - if option.OptType != OptTypeBool { - return fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, v) - } - return nil - default: - return fmt.Errorf("invalid option value type: %T", value) + return firstErr } + + return nil } // SetConfigOption sets a single value in the (prioritized) user defined config. @@ -140,9 +133,10 @@ func setConfigOption(key string, value interface{}, push bool) (err error) { if value == nil { option.activeValue = nil } else { - err = validateValue(option, value) + var valueCache *valueCache + valueCache, err = validateValue(option, value) if err == nil { - option.activeValue = value + option.activeValue = valueCache } } option.Unlock() @@ -175,9 +169,10 @@ func setDefaultConfigOption(key string, value interface{}, push bool) (err error if value == nil { option.activeDefaultValue = nil } else { - err = validateValue(option, value) + var valueCache *valueCache + valueCache, err = validateValue(option, value) if err == nil { - option.activeDefaultValue = value + option.activeDefaultValue = valueCache } } option.Unlock() diff --git a/config/validate.go b/config/validate.go new file mode 100644 index 0000000..7936bd7 --- /dev/null +++ b/config/validate.go @@ -0,0 +1,104 @@ +package config + +import ( + "errors" + "fmt" + "math" +) + +type valueCache struct { + stringVal string + stringArrayVal []string + intVal int64 + boolVal bool +} + +func validateValue(option *Option, value interface{}) (*valueCache, error) { //nolint:gocyclo + switch v := value.(type) { + case string: + if option.OptType != OptTypeString { + return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, v) + } + if option.compiledRegex != nil { + if !option.compiledRegex.MatchString(v) { + return nil, fmt.Errorf("validation of option %s failed: string \"%s\" did not match validation regex for option", option.Key, v) + } + } + return &valueCache{stringVal: v}, nil + case []interface{}: + vConverted := make([]string, len(v)) + for pos, entry := range v { + s, ok := entry.(string) + if !ok { + return nil, fmt.Errorf("validation of option %s failed: element %+v at index %d is not a string", option.Key, entry, pos) + + } + vConverted[pos] = s + } + // continue to next case + return validateValue(option, vConverted) + case []string: + if option.OptType != OptTypeStringArray { + return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, v) + } + if option.compiledRegex != nil { + for pos, entry := range v { + if !option.compiledRegex.MatchString(entry) { + return nil, fmt.Errorf("validation of option %s failed: string \"%s\" at index %d did not match validation regex", option.Key, entry, pos) + } + } + } + return &valueCache{stringArrayVal: v}, nil + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, float32, float64: + // uint64 is omitted, as it does not fit in a int64 + if option.OptType != OptTypeInt { + return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, v) + } + if option.compiledRegex != nil { + if !option.compiledRegex.MatchString(fmt.Sprintf("%d", v)) { + return nil, fmt.Errorf("validation of option %s failed: number \"%d\" did not match validation regex", option.Key, v) + } + } + switch v := value.(type) { + case int: + return &valueCache{intVal: int64(v)}, nil + case int8: + return &valueCache{intVal: int64(v)}, nil + case int16: + return &valueCache{intVal: int64(v)}, nil + case int32: + return &valueCache{intVal: int64(v)}, nil + case int64: + return &valueCache{intVal: v}, nil + case uint: + return &valueCache{intVal: int64(v)}, nil + case uint8: + return &valueCache{intVal: int64(v)}, nil + case uint16: + return &valueCache{intVal: int64(v)}, nil + case uint32: + return &valueCache{intVal: int64(v)}, nil + case float32: + // convert if float has no decimals + if math.Remainder(float64(v), 1) == 0 { + return &valueCache{intVal: int64(v)}, nil + } + return nil, fmt.Errorf("failed to convert float32 to int64 for option %s, got value %+v", option.Key, v) + case float64: + // convert if float has no decimals + if math.Remainder(v, 1) == 0 { + return &valueCache{intVal: int64(v)}, nil + } + return nil, fmt.Errorf("failed to convert float64 to int64 for option %s, got value %+v", option.Key, v) + default: + return nil, errors.New("internal error") + } + case bool: + if option.OptType != OptTypeBool { + return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, v) + } + return &valueCache{boolVal: v}, nil + default: + return nil, fmt.Errorf("invalid option value type for option %s: %T", option.Key, value) + } +}