Add option registry to config package

This commit is contained in:
Daniel 2018-08-14 16:01:09 +02:00
parent ffc13d6e16
commit 773889f66a
6 changed files with 462 additions and 17 deletions

View file

@ -43,6 +43,19 @@ func GetAsString(name string, fallback string) func() string {
}
}
// GetAsStringArray returns a function that returns the wanted string with high performance.
func GetAsStringArray(name string, fallback []string) func() []string {
valid := getValidityFlag()
value := findStringArrayValue(name, fallback)
return func() []string {
if !valid.IsSet() {
valid = getValidityFlag()
value = findStringArrayValue(name, fallback)
}
return value
}
}
// GetAsInt returns a function that returns the wanted int with high performance.
func GetAsInt(name string, fallback int64) func() int64 {
valid := getValidityFlag()
@ -55,3 +68,16 @@ func GetAsInt(name string, fallback int64) func() int64 {
return value
}
}
// GetAsBool returns a function that returns the wanted int with high performance.
func GetAsBool(name string, fallback bool) func() bool {
valid := getValidityFlag()
value := findBoolValue(name, fallback)
return func() bool {
if !valid.IsSet() {
valid = getValidityFlag()
value = findBoolValue(name, fallback)
}
return value
}
}

View file

@ -9,7 +9,10 @@ func TestGet(t *testing.T) {
err := SetConfig(`
{
"monkey": "1",
"elephant": 2
"zebra": ["black", "white"],
"elephant": 2,
"hot": true,
"cold": false
}
`)
if err != nil {
@ -28,14 +31,30 @@ func TestGet(t *testing.T) {
}
monkey := GetAsString("monkey", "none")
elephant := GetAsInt("elephant", -1)
if monkey() != "1" {
t.Fatalf("monkey should be 1, is %s", monkey())
}
zebra := GetAsStringArray("zebra", []string{})
if len(zebra()) != 2 || zebra()[0] != "black" || zebra()[1] != "white" {
t.Fatalf("zebra should be [\"black\", \"white\"], is %v", zebra())
}
elephant := GetAsInt("elephant", -1)
if elephant() != 2 {
t.Fatalf("elephant should be 2, is %d", elephant())
}
hot := GetAsBool("hot", false)
if !hot() {
t.Fatalf("hot should be true, is %v", hot())
}
cold := GetAsBool("cold", true)
if cold() {
t.Fatalf("cold should be false, is %v", cold())
}
err = SetConfig(`
{
"monkey": "3"
@ -48,10 +67,14 @@ func TestGet(t *testing.T) {
if monkey() != "3" {
t.Fatalf("monkey should be 0, is %s", monkey())
}
if elephant() != 0 {
t.Fatalf("elephant should be 0, is %d", elephant())
}
zebra()
hot()
}
func BenchmarkGetAsStringCached(b *testing.B) {

View file

@ -3,8 +3,10 @@ package config
import (
"errors"
"sync"
"fmt"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
var (
@ -15,6 +17,9 @@ var (
// ErrInvalidJSON is returned by SetConfig and SetDefaultConfig if they receive invalid json.
ErrInvalidJSON = errors.New("json string invalid")
// ErrInvalidOptionType is returned by SetConfigOption and SetDefaultConfigOption if given an unsupported option type.
ErrInvalidOptionType = errors.New("invalid option value type")
)
// SetConfig sets the (prioritized) user defined config.
@ -45,7 +50,115 @@ func SetDefaultConfig(json string) error {
return nil
}
// findValue find the correct value in the user or default config
func validateValue(name string, value interface{}) error {
optionsLock.RLock()
defer optionsLock.RUnlock()
option, ok := options[name]
if !ok {
switch value.(type) {
case string:
return nil
case []string:
return nil
case int:
return nil
case bool:
return nil
default:
return ErrInvalidOptionType
}
}
switch v := value.(type) {
case string:
if option.OptType != OptTypeString {
return fmt.Errorf("expected type string for option %s, got type %T", name, 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, name)
}
}
return nil
case []string:
if option.OptType != OptTypeStringArray {
return fmt.Errorf("expected type string for option %s, got type %T", name, 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, name)
}
}
}
return nil
case int:
if option.OptType != OptTypeInt {
return fmt.Errorf("expected type int for option %s, got type %T", name, v)
}
return nil
case bool:
if option.OptType != OptTypeBool {
return fmt.Errorf("expected type bool for option %s, got type %T", name, v)
}
return nil
default:
return ErrInvalidOptionType
}
}
// SetConfigOption sets a single value in the (prioritized) user defined config.
func SetConfigOption(name string, value interface{}) error {
configLock.Lock()
defer configLock.Unlock()
var err error
var newConfig string
if value == nil {
newConfig, err = sjson.Delete(userConfig, name)
} else {
err = validateValue(name, value)
if err == nil {
newConfig, err = sjson.Set(userConfig, name, value)
}
}
if err == nil {
userConfig = newConfig
resetValidityFlag()
}
return err
}
// SetDefaultConfigOption sets a single value in the (fallback) default config.
func SetDefaultConfigOption(name string, value interface{}) error {
configLock.Lock()
defer configLock.Unlock()
var err error
var newConfig string
if value == nil {
newConfig, err = sjson.Delete(defaultConfig, name)
} else {
err = validateValue(name, value)
if err == nil {
newConfig, err = sjson.Set(defaultConfig, name, value)
}
}
if err == nil {
defaultConfig = newConfig
resetValidityFlag()
}
return err
}
// findValue find the correct value in the user or default config.
func findValue(name string) (result gjson.Result) {
configLock.RLock()
defer configLock.RUnlock()
@ -57,7 +170,7 @@ func findValue(name string) (result gjson.Result) {
return result
}
// findStringValue validates and return the value with the given name
// findStringValue validates and returns the value with the given name.
func findStringValue(name string, fallback string) (value string) {
result := findValue(name)
if !result.Exists() {
@ -69,7 +182,26 @@ func findStringValue(name string, fallback string) (value string) {
return result.String()
}
// findIntValue validates and return the value with the given name
// findStringArrayValue validates and returns the value with the given name.
func findStringArrayValue(name string, fallback []string) (value []string) {
result := findValue(name)
if !result.Exists() {
return fallback
}
if !result.IsArray() {
return fallback
}
results := result.Array()
for _, r := range results {
if r.Type != gjson.String {
return fallback
}
value = append(value, r.String())
}
return value
}
// findIntValue validates and returns the value with the given name.
func findIntValue(name string, fallback int64) (value int64) {
result := findValue(name)
if !result.Exists() {
@ -80,3 +212,19 @@ func findIntValue(name string, fallback int64) (value int64) {
}
return result.Int()
}
// findBoolValue validates and returns the value with the given name.
func findBoolValue(name string, fallback bool) (value bool) {
result := findValue(name)
if !result.Exists() {
return fallback
}
switch result.Type {
case gjson.True:
return true
case gjson.False:
return false
default:
return fallback
}
}

View file

@ -2,50 +2,186 @@ package config
import "testing"
func TestLayers(t *testing.T) {
func TestLayersGetters(t *testing.T) {
err := SetConfig("{invalid json")
if err == nil {
t.Fatal("expected error")
t.Error("expected error")
}
err = SetDefaultConfig("{invalid json")
if err == nil {
t.Fatal("expected error")
t.Error("expected error")
}
err = SetConfig(`
{
"monkey": "banana",
"elephant": 3
"monkey": "1",
"zebra": ["black", "white"],
"weird_zebra": ["black", -1],
"elephant": 2,
"hot": true
}
`)
if err != nil {
t.Fatal(err)
t.Error(err)
}
// Test missing values
missingString := GetAsString("missing", "fallback")
if missingString() != "fallback" {
t.Fatal("expected fallback value: fallback")
t.Error("expected fallback value: fallback")
}
missingStringArray := GetAsStringArray("missing", []string{"fallback"})
if len(missingStringArray()) != 1 || missingStringArray()[0] != "fallback" {
t.Error("expected fallback value: [fallback]")
}
missingInt := GetAsInt("missing", -1)
if missingInt() != -1 {
t.Fatal("expected fallback value: -1")
t.Error("expected fallback value: -1")
}
missingBool := GetAsBool("missing", false)
if missingBool() {
t.Error("expected fallback value: false")
}
// Test value mismatch
notString := GetAsString("elephant", "fallback")
if notString() != "fallback" {
t.Fatal("expected fallback value: fallback")
t.Error("expected fallback value: fallback")
}
notStringArray := GetAsStringArray("elephant", []string{"fallback"})
if len(notStringArray()) != 1 || notStringArray()[0] != "fallback" {
t.Error("expected fallback value: [fallback]")
}
mixedStringArray := GetAsStringArray("weird_zebra", []string{"fallback"})
if len(mixedStringArray()) != 1 || mixedStringArray()[0] != "fallback" {
t.Error("expected fallback value: [fallback]")
}
notInt := GetAsInt("monkey", -1)
if notInt() != -1 {
t.Fatal("expected fallback value: -1")
t.Error("expected fallback value: -1")
}
notBool := GetAsBool("monkey", false)
if notBool() {
t.Error("expected fallback value: false")
}
}
func TestLayersSetters(t *testing.T) {
Register(&Option{
Name: "name",
Key: "monkey",
Description: "description",
ExpertiseLevel: 1,
OptType: OptTypeString,
DefaultValue: "banana",
ValidationRegex: "^(banana|water)$",
})
Register(&Option{
Name: "name",
Key: "zebra",
Description: "description",
ExpertiseLevel: 1,
OptType: OptTypeStringArray,
DefaultValue: []string{"black", "white"},
ValidationRegex: "^[a-z]+$",
})
Register(&Option{
Name: "name",
Key: "elephant",
Description: "description",
ExpertiseLevel: 1,
OptType: OptTypeInt,
DefaultValue: 2,
ValidationRegex: "",
})
Register(&Option{
Name: "name",
Key: "hot",
Description: "description",
ExpertiseLevel: 1,
OptType: OptTypeBool,
DefaultValue: true,
ValidationRegex: "",
})
// correct types
if err := SetConfigOption("monkey", "banana"); err != nil {
t.Error(err)
}
if err := SetConfigOption("zebra", []string{"black", "white"}); err != nil {
t.Error(err)
}
if err := SetDefaultConfigOption("elephant", 2); err != nil {
t.Error(err)
}
if err := SetDefaultConfigOption("hot", true); err != nil {
t.Error(err)
}
// incorrect types
if err := SetConfigOption("monkey", []string{"black", "white"}); err == nil {
t.Error("should fail")
}
if err := SetConfigOption("zebra", 2); err == nil {
t.Error("should fail")
}
if err := SetDefaultConfigOption("elephant", true); err == nil {
t.Error("should fail")
}
if err := SetDefaultConfigOption("hot", "banana"); err == nil {
t.Error("should fail")
}
if err := SetDefaultConfigOption("hot", []byte{0}); err == nil {
t.Error("should fail")
}
// validation fail
if err := SetConfigOption("monkey", "dirt"); err == nil {
t.Error("should fail")
}
if err := SetConfigOption("zebra", []string{"Element649"}); err == nil {
t.Error("should fail")
}
// unregistered checking
if err := SetConfigOption("invalid", "banana"); err != nil {
t.Error(err)
}
if err := SetConfigOption("invalid", []string{"black", "white"}); err != nil {
t.Error(err)
}
if err := SetConfigOption("invalid", 2); err != nil {
t.Error(err)
}
if err := SetConfigOption("invalid", true); err != nil {
t.Error(err)
}
if err := SetConfigOption("invalid", []byte{0}); err != ErrInvalidOptionType {
t.Error("should fail with ErrInvalidOptionType")
}
// delete
if err := SetConfigOption("monkey", nil); err != nil {
t.Error(err)
}
if err := SetDefaultConfigOption("elephant", nil); err != nil {
t.Error(err)
}
if err := SetDefaultConfigOption("invalid_delete", nil); err != nil {
t.Error(err)
}
}

67
config/registry.go Normal file
View file

@ -0,0 +1,67 @@
package config
import (
"errors"
"fmt"
"regexp"
"sync"
)
// Variable Type IDs for frontend Identification. Values over 100 are free for custom use.
const (
OptTypeString uint8 = 1
OptTypeStringArray uint8 = 2
OptTypeInt uint8 = 3
OptTypeBool uint8 = 4
ExpertiseLevelUser int8 = 1
ExpertiseLevelExpert int8 = 2
ExpertiseLevelDeveloper int8 = 3
)
var (
optionsLock sync.RWMutex
options = make(map[string]*Option)
// ErrIncompleteCall is return when RegisterOption is called with empty mandatory values.
ErrIncompleteCall = errors.New("could not register config option: all fields, except for the validationRegex are mandatory")
)
// Option describes a configuration option.
type Option struct {
Name string
Key string
Description string
ExpertiseLevel uint8
OptType uint8
DefaultValue interface{}
ValidationRegex string
compiledRegex *regexp.Regexp
}
// Register registers a new configuration option.
func Register(option *Option) error {
if option.Name == "" ||
option.Key == "" ||
option.Description == "" ||
option.ExpertiseLevel == 0 ||
option.OptType == 0 {
return ErrIncompleteCall
}
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)
}
}
optionsLock.Lock()
defer optionsLock.Unlock()
options[option.Key] = option
return nil
}

45
config/registry_test.go Normal file
View file

@ -0,0 +1,45 @@
package config
import (
"testing"
)
func TestRegistry(t *testing.T) {
if err := Register(&Option{
Name: "name",
Key: "key",
Description: "description",
ExpertiseLevel: 1,
OptType: OptTypeString,
DefaultValue: "default",
ValidationRegex: "^(banana|water)$",
}); err != nil {
t.Error(err)
}
if err := Register(&Option{
Name: "name",
Key: "key",
Description: "description",
ExpertiseLevel: 1,
OptType: 0,
DefaultValue: "default",
ValidationRegex: "^[A-Z][a-z]+$",
}); err == nil {
t.Error("should fail")
}
if err := Register(&Option{
Name: "name",
Key: "key",
Description: "description",
ExpertiseLevel: 1,
OptType: OptTypeString,
DefaultValue: "default",
ValidationRegex: "[",
}); err == nil {
t.Error("should fail")
}
}