Add perspective, improve getters

This commit is contained in:
Daniel 2020-03-20 17:24:19 +01:00
parent be456c9d64
commit e349fb4f5b
11 changed files with 583 additions and 230 deletions

View file

@ -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:

View file

@ -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
}

View file

@ -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
}
*/

View file

@ -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)
}
}

View file

@ -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.

128
config/perspective.go Normal file
View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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()

104
config/validate.go Normal file
View file

@ -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)
}
}