mirror of
https://github.com/safing/portbase
synced 2025-09-01 18:19:57 +00:00
Add option registry to config package
This commit is contained in:
parent
ffc13d6e16
commit
773889f66a
6 changed files with 462 additions and 17 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
154
config/layers.go
154
config/layers.go
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
67
config/registry.go
Normal 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
45
config/registry_test.go
Normal 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")
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue