mirror of
https://github.com/safing/portbase
synced 2025-09-01 10:09:50 +00:00
Add perspective, improve getters
This commit is contained in:
parent
be456c9d64
commit
e349fb4f5b
11 changed files with 583 additions and 230 deletions
|
@ -5,6 +5,8 @@ package config
|
|||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
// Expertise Level constants
|
||||
|
@ -22,6 +24,8 @@ const (
|
|||
|
||||
var (
|
||||
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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
204
config/get.go
204
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
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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,7 +49,7 @@ 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)
|
||||
|
@ -56,7 +57,7 @@ func TestGet(t *testing.T) {
|
|||
|
||||
err = parseAndSetConfig(`
|
||||
{
|
||||
"monkey": "1",
|
||||
"monkey": "a",
|
||||
"zebras": {
|
||||
"zebra": ["black", "white"]
|
||||
},
|
||||
|
@ -71,7 +72,7 @@ func TestGet(t *testing.T) {
|
|||
|
||||
err = parseAndSetDefaultConfig(`
|
||||
{
|
||||
"monkey": "0",
|
||||
"monkey": "b",
|
||||
"snake": "0",
|
||||
"elephant": 0
|
||||
}
|
||||
|
@ -81,8 +82,8 @@ func TestGet(t *testing.T) {
|
|||
}
|
||||
|
||||
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{})
|
||||
|
@ -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(`
|
||||
{
|
||||
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(`
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,8 +53,9 @@ 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)
|
||||
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
|
||||
}
|
||||
|
||||
|
|
128
config/perspective.go
Normal file
128
config/perspective.go
Normal 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
|
||||
}
|
|
@ -26,14 +26,20 @@ func Register(option *Option) error {
|
|||
return fmt.Errorf("failed to register option: please set option.OptType")
|
||||
}
|
||||
|
||||
if option.ValidationRegex != "" {
|
||||
var err error
|
||||
|
||||
if option.ValidationRegex != "" {
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -5,6 +5,8 @@ package config
|
|||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
// Release Level constants
|
||||
|
@ -22,6 +24,8 @@ const (
|
|||
|
||||
var (
|
||||
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:
|
||||
|
|
101
config/set.go
101
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()
|
||||
if ok {
|
||||
option.activeValue = newValue
|
||||
} else {
|
||||
option.activeValue = nil
|
||||
if ok {
|
||||
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()
|
||||
if ok {
|
||||
option.activeDefaultValue = newValue
|
||||
} else {
|
||||
option.activeDefaultValue = nil
|
||||
if ok {
|
||||
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
|
||||
|
||||
if firstErr != nil {
|
||||
if errCnt > 0 {
|
||||
return fmt.Errorf("encountered %d errors, first was: %s", errCnt, firstErr)
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
104
config/validate.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue