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 ( import (
"fmt" "fmt"
"sync/atomic" "sync/atomic"
"github.com/tevino/abool"
) )
// Expertise Level constants // Expertise Level constants
@ -21,7 +23,9 @@ const (
) )
var ( var (
expertiseLevel *int32 expertiseLevel *int32
expertiseLevelOption *Option
expertiseLevelOptionFlag = abool.New()
) )
func init() { func init() {
@ -32,7 +36,7 @@ func init() {
} }
func registerExpertiseLevelOption() { func registerExpertiseLevelOption() {
err := Register(&Option{ expertiseLevelOption = &Option{
Name: "Expertise Level", Name: "Expertise Level",
Key: expertiseLevelKey, 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)", 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", ExternalOptType: "string list",
ValidationRegex: fmt.Sprintf("^(%s|%s|%s)$", ExpertiseLevelNameUser, ExpertiseLevelNameExpert, ExpertiseLevelNameDeveloper), ValidationRegex: fmt.Sprintf("^(%s|%s|%s)$", ExpertiseLevelNameUser, ExpertiseLevelNameExpert, ExpertiseLevelNameDeveloper),
}) }
err := Register(expertiseLevelOption)
if err != nil { if err != nil {
panic(err) panic(err)
} }
expertiseLevelOptionFlag.Set()
} }
func updateExpertiseLevel() { func updateExpertiseLevel() {
new := findStringValue(expertiseLevelKey, "") // check if already registered
switch new { 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: case ExpertiseLevelNameUser:
atomic.StoreInt32(expertiseLevel, int32(ExpertiseLevelUser)) atomic.StoreInt32(expertiseLevel, int32(ExpertiseLevelUser))
case ExpertiseLevelNameExpert: case ExpertiseLevelNameExpert:

View file

@ -11,15 +11,25 @@ var (
// GetAsString returns a function that returns the wanted string with high performance. // GetAsString returns a function that returns the wanted string with high performance.
func (cs *safe) GetAsString(name string, fallback string) StringOption { func (cs *safe) GetAsString(name string, fallback string) StringOption {
valid := getValidityFlag() valid := GetValidityFlag()
value := findStringValue(name, fallback) option, valueCache := getValueCache(name, nil, OptTypeString)
value := fallback
if valueCache != nil {
value = valueCache.stringVal
}
var lock sync.Mutex var lock sync.Mutex
return func() string { return func() string {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
if !valid.IsSet() { if !valid.IsSet() {
valid = getValidityFlag() valid = GetValidityFlag()
value = findStringValue(name, fallback) option, valueCache = getValueCache(name, option, OptTypeString)
if valueCache != nil {
value = valueCache.stringVal
} else {
value = fallback
}
} }
return value 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. // GetAsStringArray returns a function that returns the wanted string with high performance.
func (cs *safe) GetAsStringArray(name string, fallback []string) StringArrayOption { func (cs *safe) GetAsStringArray(name string, fallback []string) StringArrayOption {
valid := getValidityFlag() valid := GetValidityFlag()
value := findStringArrayValue(name, fallback) option, valueCache := getValueCache(name, nil, OptTypeStringArray)
value := fallback
if valueCache != nil {
value = valueCache.stringArrayVal
}
var lock sync.Mutex var lock sync.Mutex
return func() []string { return func() []string {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
if !valid.IsSet() { if !valid.IsSet() {
valid = getValidityFlag() valid = GetValidityFlag()
value = findStringArrayValue(name, fallback) option, valueCache = getValueCache(name, option, OptTypeStringArray)
if valueCache != nil {
value = valueCache.stringArrayVal
} else {
value = fallback
}
} }
return value 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. // GetAsInt returns a function that returns the wanted int with high performance.
func (cs *safe) GetAsInt(name string, fallback int64) IntOption { func (cs *safe) GetAsInt(name string, fallback int64) IntOption {
valid := getValidityFlag() valid := GetValidityFlag()
value := findIntValue(name, fallback) option, valueCache := getValueCache(name, nil, OptTypeInt)
value := fallback
if valueCache != nil {
value = valueCache.intVal
}
var lock sync.Mutex var lock sync.Mutex
return func() int64 { return func() int64 {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
if !valid.IsSet() { if !valid.IsSet() {
valid = getValidityFlag() valid = GetValidityFlag()
value = findIntValue(name, fallback) option, valueCache = getValueCache(name, option, OptTypeInt)
if valueCache != nil {
value = valueCache.intVal
} else {
value = fallback
}
} }
return value 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. // GetAsBool returns a function that returns the wanted int with high performance.
func (cs *safe) GetAsBool(name string, fallback bool) BoolOption { func (cs *safe) GetAsBool(name string, fallback bool) BoolOption {
valid := getValidityFlag() valid := GetValidityFlag()
value := findBoolValue(name, fallback) option, valueCache := getValueCache(name, nil, OptTypeBool)
value := fallback
if valueCache != nil {
value = valueCache.boolVal
}
var lock sync.Mutex var lock sync.Mutex
return func() bool { return func() bool {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
if !valid.IsSet() { if !valid.IsSet() {
valid = getValidityFlag() valid = GetValidityFlag()
value = findBoolValue(name, fallback) option, valueCache = getValueCache(name, option, OptTypeBool)
if valueCache != nil {
value = valueCache.boolVal
} else {
value = fallback
}
} }
return value return value
} }

View file

@ -15,14 +15,59 @@ type (
BoolOption func() bool 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. // GetAsString returns a function that returns the wanted string with high performance.
func GetAsString(name string, fallback string) StringOption { func GetAsString(name string, fallback string) StringOption {
valid := getValidityFlag() valid := GetValidityFlag()
value := findStringValue(name, fallback) option, valueCache := getValueCache(name, nil, OptTypeString)
value := fallback
if valueCache != nil {
value = valueCache.stringVal
}
return func() string { return func() string {
if !valid.IsSet() { if !valid.IsSet() {
valid = getValidityFlag() valid = GetValidityFlag()
value = findStringValue(name, fallback) option, valueCache = getValueCache(name, option, OptTypeString)
if valueCache != nil {
value = valueCache.stringVal
} else {
value = fallback
}
} }
return value 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. // GetAsStringArray returns a function that returns the wanted string with high performance.
func GetAsStringArray(name string, fallback []string) StringArrayOption { func GetAsStringArray(name string, fallback []string) StringArrayOption {
valid := getValidityFlag() valid := GetValidityFlag()
value := findStringArrayValue(name, fallback) option, valueCache := getValueCache(name, nil, OptTypeStringArray)
value := fallback
if valueCache != nil {
value = valueCache.stringArrayVal
}
return func() []string { return func() []string {
if !valid.IsSet() { if !valid.IsSet() {
valid = getValidityFlag() valid = GetValidityFlag()
value = findStringArrayValue(name, fallback) option, valueCache = getValueCache(name, option, OptTypeStringArray)
if valueCache != nil {
value = valueCache.stringArrayVal
} else {
value = fallback
}
} }
return value 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. // GetAsInt returns a function that returns the wanted int with high performance.
func GetAsInt(name string, fallback int64) IntOption { func GetAsInt(name string, fallback int64) IntOption {
valid := getValidityFlag() valid := GetValidityFlag()
value := findIntValue(name, fallback) option, valueCache := getValueCache(name, nil, OptTypeInt)
value := fallback
if valueCache != nil {
value = valueCache.intVal
}
return func() int64 { return func() int64 {
if !valid.IsSet() { if !valid.IsSet() {
valid = getValidityFlag() valid = GetValidityFlag()
value = findIntValue(name, fallback) option, valueCache = getValueCache(name, option, OptTypeInt)
if valueCache != nil {
value = valueCache.intVal
} else {
value = fallback
}
} }
return value 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. // GetAsBool returns a function that returns the wanted int with high performance.
func GetAsBool(name string, fallback bool) BoolOption { func GetAsBool(name string, fallback bool) BoolOption {
valid := getValidityFlag() valid := GetValidityFlag()
value := findBoolValue(name, fallback) option, valueCache := getValueCache(name, nil, OptTypeBool)
value := fallback
if valueCache != nil {
value = valueCache.boolVal
}
return func() bool { return func() bool {
if !valid.IsSet() { if !valid.IsSet() {
valid = getValidityFlag() valid = GetValidityFlag()
value = findBoolValue(name, fallback) option, valueCache = getValueCache(name, option, OptTypeBool)
if valueCache != nil {
value = valueCache.boolVal
} else {
value = fallback
}
} }
return value 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() optionsLock.RLock()
option, ok := options[key] option, ok := options[key]
optionsLock.RUnlock() optionsLock.RUnlock()
@ -77,6 +152,13 @@ func findValue(key string) interface{} {
return nil return nil
} }
return option.findValue()
}
*/
/*
// findValue finds the preferred value in the user or default config.
func (option *Option) findValue() interface{} {
// lock option // lock option
option.Lock() option.Lock()
defer option.Unlock() defer option.Unlock()
@ -91,88 +173,4 @@ func findValue(key string) interface{} {
return option.DefaultValue 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 package config
import ( import (
"encoding/json"
"testing" "testing"
"github.com/safing/portbase/log" "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 // reset
options = make(map[string]*Option) options = make(map[string]*Option)
@ -48,41 +49,41 @@ func TestGet(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
quickRegister(t, "monkey", OptTypeInt, -1) quickRegister(t, "monkey", OptTypeString, "c")
quickRegister(t, "zebras/zebra", OptTypeStringArray, []string{"a", "b"}) quickRegister(t, "zebras/zebra", OptTypeStringArray, []string{"a", "b"})
quickRegister(t, "elephant", OptTypeInt, -1) quickRegister(t, "elephant", OptTypeInt, -1)
quickRegister(t, "hot", OptTypeBool, false) quickRegister(t, "hot", OptTypeBool, false)
quickRegister(t, "cold", OptTypeBool, true) quickRegister(t, "cold", OptTypeBool, true)
err = parseAndSetConfig(` err = parseAndSetConfig(`
{ {
"monkey": "1", "monkey": "a",
"zebras": { "zebras": {
"zebra": ["black", "white"] "zebra": ["black", "white"]
}, },
"elephant": 2, "elephant": 2,
"hot": true, "hot": true,
"cold": false "cold": false
} }
`) `)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
err = parseAndSetDefaultConfig(` err = parseAndSetDefaultConfig(`
{ {
"monkey": "0", "monkey": "b",
"snake": "0", "snake": "0",
"elephant": 0 "elephant": 0
} }
`) `)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
monkey := GetAsString("monkey", "none") monkey := GetAsString("monkey", "none")
if monkey() != "1" { if monkey() != "a" {
t.Errorf("monkey should be 1, is %s", monkey()) t.Errorf("monkey should be a, is %s", monkey())
} }
zebra := GetAsStringArray("zebras/zebra", []string{}) zebra := GetAsStringArray("zebras/zebra", []string{})
@ -106,10 +107,10 @@ func TestGet(t *testing.T) {
} }
err = parseAndSetConfig(` err = parseAndSetConfig(`
{ {
"monkey": "3" "monkey": "3"
} }
`) `)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -131,6 +132,53 @@ func TestGet(t *testing.T) {
GetAsInt("elephant", -1)() GetAsInt("elephant", -1)()
GetAsBool("hot", false)() 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) { func TestReleaseLevel(t *testing.T) {
@ -236,11 +284,9 @@ func BenchmarkGetAsStringCached(b *testing.B) {
options = make(map[string]*Option) options = make(map[string]*Option)
// Setup // Setup
err := parseAndSetConfig(` err := parseAndSetConfig(`{
{ "monkey": "banana"
"monkey": "banana" }`)
}
`)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@ -257,11 +303,9 @@ func BenchmarkGetAsStringCached(b *testing.B) {
func BenchmarkGetAsStringRefetch(b *testing.B) { func BenchmarkGetAsStringRefetch(b *testing.B) {
// Setup // Setup
err := parseAndSetConfig(` err := parseAndSetConfig(`{
{ "monkey": "banana"
"monkey": "banana" }`)
}
`)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@ -271,38 +315,34 @@ func BenchmarkGetAsStringRefetch(b *testing.B) {
// Start benchmark // Start benchmark
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
findStringValue("monkey", "no banana") getValueCache("monkey", nil, OptTypeString)
} }
} }
func BenchmarkGetAsIntCached(b *testing.B) { func BenchmarkGetAsIntCached(b *testing.B) {
// Setup // Setup
err := parseAndSetConfig(` err := parseAndSetConfig(`{
{ "elephant": 1
"monkey": 1 }`)
}
`)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
monkey := GetAsInt("monkey", -1) elephant := GetAsInt("elephant", -1)
// Reset timer for precise results // Reset timer for precise results
b.ResetTimer() b.ResetTimer()
// Start benchmark // Start benchmark
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
monkey() elephant()
} }
} }
func BenchmarkGetAsIntRefetch(b *testing.B) { func BenchmarkGetAsIntRefetch(b *testing.B) {
// Setup // Setup
err := parseAndSetConfig(` err := parseAndSetConfig(`{
{ "elephant": 1
"monkey": 1 }`)
}
`)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@ -312,6 +352,6 @@ func BenchmarkGetAsIntRefetch(b *testing.B) {
// Start benchmark // Start benchmark
for i := 0; i < b.N; i++ { 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 Name string
Key string // in path format: category/sub/key Key string // in path format: category/sub/key
Description string Description string
Help string
OptType uint8 OptType uint8
ExpertiseLevel uint8 ExpertiseLevel uint8
@ -52,9 +53,10 @@ type Option struct {
ExternalOptType string ExternalOptType string
ValidationRegex string ValidationRegex string
activeValue interface{} // runtime value (loaded from config file or set by user) activeValue *valueCache // runtime value (loaded from config file or set by user)
activeDefaultValue interface{} // runtime default value (may be set internally) activeDefaultValue *valueCache // runtime default value (may be set internally)
compiledRegex *regexp.Regexp activeFallbackValue *valueCache // default value from option registration
compiledRegex *regexp.Regexp
} }
// Export expors an option to a Record. // 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") return fmt.Errorf("failed to register option: please set option.OptType")
} }
var err error
if option.ValidationRegex != "" { if option.ValidationRegex != "" {
var err error
option.compiledRegex, err = regexp.Compile(option.ValidationRegex) option.compiledRegex, err = regexp.Compile(option.ValidationRegex)
if err != nil { if err != nil {
return fmt.Errorf("config: could not compile option.ValidationRegex: %s", err) 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() optionsLock.Lock()
defer optionsLock.Unlock() defer optionsLock.Unlock()
options[option.Key] = option options[option.Key] = option

View file

@ -15,7 +15,7 @@ func TestRegistry(t *testing.T) {
ReleaseLevel: ReleaseLevelStable, ReleaseLevel: ReleaseLevelStable,
ExpertiseLevel: ExpertiseLevelUser, ExpertiseLevel: ExpertiseLevelUser,
OptType: OptTypeString, OptType: OptTypeString,
DefaultValue: "default", DefaultValue: "water",
ValidationRegex: "^(banana|water)$", ValidationRegex: "^(banana|water)$",
}); err != nil { }); err != nil {
t.Error(err) t.Error(err)

View file

@ -5,6 +5,8 @@ package config
import ( import (
"fmt" "fmt"
"sync/atomic" "sync/atomic"
"github.com/tevino/abool"
) )
// Release Level constants // Release Level constants
@ -21,7 +23,9 @@ const (
) )
var ( var (
releaseLevel *int32 releaseLevel *int32
releaseLevelOption *Option
releaseLevelOptionFlag = abool.New()
) )
func init() { func init() {
@ -32,7 +36,7 @@ func init() {
} }
func registerReleaseLevelOption() { func registerReleaseLevelOption() {
err := Register(&Option{ releaseLevelOption = &Option{
Name: "Release Level", Name: "Release Level",
Key: releaseLevelKey, 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.", 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", ExternalOptType: "string list",
ValidationRegex: fmt.Sprintf("^(%s|%s|%s)$", ReleaseLevelNameStable, ReleaseLevelNameBeta, ReleaseLevelNameExperimental), ValidationRegex: fmt.Sprintf("^(%s|%s|%s)$", ReleaseLevelNameStable, ReleaseLevelNameBeta, ReleaseLevelNameExperimental),
}) }
err := Register(releaseLevelOption)
if err != nil { if err != nil {
panic(err) panic(err)
} }
releaseLevelOptionFlag.Set()
} }
func updateReleaseLevel() { func updateReleaseLevel() {
new := findStringValue(releaseLevelKey, "") // check if already registered
switch new { 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: case ReleaseLevelNameStable:
atomic.StoreInt32(releaseLevel, int32(ReleaseLevelStable)) atomic.StoreInt32(releaseLevel, int32(ReleaseLevelStable))
case ReleaseLevelNameBeta: case ReleaseLevelNameBeta:

View file

@ -19,7 +19,8 @@ var (
validityFlagLock sync.RWMutex 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() validityFlagLock.RLock()
defer validityFlagLock.RUnlock() defer validityFlagLock.RUnlock()
return validityFlag return validityFlag
@ -41,14 +42,24 @@ func signalChanges() {
// setConfig sets the (prioritized) user defined config. // setConfig sets the (prioritized) user defined config.
func setConfig(newValues map[string]interface{}) error { func setConfig(newValues map[string]interface{}) error {
var firstErr error
var errCnt int
optionsLock.Lock() optionsLock.Lock()
for key, option := range options { for key, option := range options {
newValue, ok := newValues[key] newValue, ok := newValues[key]
option.Lock() option.Lock()
option.activeValue = nil
if ok { if ok {
option.activeValue = newValue valueCache, err := validateValue(option, newValue)
} else { if err == nil {
option.activeValue = nil option.activeValue = valueCache
} else {
errCnt++
if firstErr == nil {
firstErr = err
}
}
} }
option.Unlock() option.Unlock()
} }
@ -56,19 +67,37 @@ func setConfig(newValues map[string]interface{}) error {
signalChanges() signalChanges()
go pushFullUpdate() go pushFullUpdate()
if firstErr != nil {
if errCnt > 0 {
return fmt.Errorf("encountered %d errors, first was: %s", errCnt, firstErr)
}
return firstErr
}
return nil return nil
} }
// SetDefaultConfig sets the (fallback) default config. // SetDefaultConfig sets the (fallback) default config.
func SetDefaultConfig(newValues map[string]interface{}) error { func SetDefaultConfig(newValues map[string]interface{}) error {
var firstErr error
var errCnt int
optionsLock.Lock() optionsLock.Lock()
for key, option := range options { for key, option := range options {
newValue, ok := newValues[key] newValue, ok := newValues[key]
option.Lock() option.Lock()
option.activeDefaultValue = nil
if ok { if ok {
option.activeDefaultValue = newValue valueCache, err := validateValue(option, newValue)
} else { if err == nil {
option.activeDefaultValue = nil option.activeDefaultValue = valueCache
} else {
errCnt++
if firstErr == nil {
firstErr = err
}
}
} }
option.Unlock() option.Unlock()
} }
@ -76,51 +105,15 @@ func SetDefaultConfig(newValues map[string]interface{}) error {
signalChanges() signalChanges()
go pushFullUpdate() go pushFullUpdate()
return nil
}
func validateValue(option *Option, value interface{}) error { if firstErr != nil {
switch v := value.(type) { if errCnt > 0 {
case string: return fmt.Errorf("encountered %d errors, first was: %s", errCnt, firstErr)
if option.OptType != OptTypeString {
return fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, v)
} }
if option.compiledRegex != nil { return firstErr
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 nil
} }
// SetConfigOption sets a single value in the (prioritized) user defined config. // 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 { if value == nil {
option.activeValue = nil option.activeValue = nil
} else { } else {
err = validateValue(option, value) var valueCache *valueCache
valueCache, err = validateValue(option, value)
if err == nil { if err == nil {
option.activeValue = value option.activeValue = valueCache
} }
} }
option.Unlock() option.Unlock()
@ -175,9 +169,10 @@ func setDefaultConfigOption(key string, value interface{}, push bool) (err error
if value == nil { if value == nil {
option.activeDefaultValue = nil option.activeDefaultValue = nil
} else { } else {
err = validateValue(option, value) var valueCache *valueCache
valueCache, err = validateValue(option, value)
if err == nil { if err == nil {
option.activeDefaultValue = value option.activeDefaultValue = valueCache
} }
} }
option.Unlock() 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)
}
}