mirror of
https://github.com/safing/portbase
synced 2025-09-05 04:00:14 +00:00
Improve config and integrate with database
This commit is contained in:
parent
31c09512a0
commit
53fde29e1a
12 changed files with 672 additions and 157 deletions
155
config/database.go
Normal file
155
config/database.go
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/log"
|
||||||
|
"github.com/Safing/portbase/database"
|
||||||
|
"github.com/Safing/portbase/database/storage"
|
||||||
|
"github.com/Safing/portbase/database/record"
|
||||||
|
"github.com/Safing/portbase/database/query"
|
||||||
|
"github.com/Safing/portbase/database/iterator"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dbController *database.Controller
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigStorageInterface provices a storage.Interface to the configuration manager.
|
||||||
|
type ConfigStorageInterface struct {
|
||||||
|
storage.InjectBase
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a database record.
|
||||||
|
func (s *ConfigStorageInterface) Get(key string) (record.Record, error) {
|
||||||
|
optionsLock.Lock()
|
||||||
|
defer optionsLock.Unlock()
|
||||||
|
|
||||||
|
opt, ok := options[key]
|
||||||
|
if !ok {
|
||||||
|
return nil, storage.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return opt.Export()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put stores a record in the database.
|
||||||
|
func (s *ConfigStorageInterface) Put(r record.Record) error {
|
||||||
|
acc := r.GetAccessor(r)
|
||||||
|
if acc == nil {
|
||||||
|
return errors.New("invalid data")
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsLock.RLock()
|
||||||
|
option, ok := options[r.DatabaseKey()]
|
||||||
|
optionsLock.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return errors.New("config option does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
var value interface{}
|
||||||
|
switch option.OptType {
|
||||||
|
case OptTypeString :
|
||||||
|
value, ok = acc.GetString("Value")
|
||||||
|
case OptTypeStringArray :
|
||||||
|
value, ok = acc.GetStringArray("Value")
|
||||||
|
case OptTypeInt :
|
||||||
|
value, ok = acc.GetInt("Value")
|
||||||
|
case OptTypeBool :
|
||||||
|
value, ok = acc.GetBool("Value")
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return errors.New("expected new value in \"Value\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := setConfigOption(r.DatabaseKey(), value, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a record from the database.
|
||||||
|
func (s *ConfigStorageInterface) Delete(key string) error {
|
||||||
|
return setConfigOption(key, nil, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query returns a an iterator for the supplied query.
|
||||||
|
func (s *ConfigStorageInterface) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) {
|
||||||
|
|
||||||
|
optionsLock.Lock()
|
||||||
|
defer optionsLock.Unlock()
|
||||||
|
|
||||||
|
it := iterator.New()
|
||||||
|
var opts []*Option
|
||||||
|
for _, opt := range options {
|
||||||
|
if strings.HasPrefix(opt.Key, q.DatabaseKeyPrefix()) {
|
||||||
|
opts = append(opts, opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.processQuery(q, it, opts)
|
||||||
|
|
||||||
|
return it, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigStorageInterface) processQuery(q *query.Query, it *iterator.Iterator, opts []*Option) {
|
||||||
|
|
||||||
|
sort.Sort(sortableOptions(opts))
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
r, err := opt.Export()
|
||||||
|
if err != nil {
|
||||||
|
it.Finish(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it.Next <- r
|
||||||
|
}
|
||||||
|
|
||||||
|
it.Finish(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadOnly returns whether the database is read only.
|
||||||
|
func (s *ConfigStorageInterface) ReadOnly() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerAsDatabase() error {
|
||||||
|
_, err := database.Register(&database.Database{
|
||||||
|
Name: "config",
|
||||||
|
Description: "Configuration Manager",
|
||||||
|
StorageType: "injected",
|
||||||
|
PrimaryAPI: "",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
controller, err := database.InjectDatabase("config", &ConfigStorageInterface{})
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dbController = controller
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushFullUpdate() {
|
||||||
|
optionsLock.RLock()
|
||||||
|
defer optionsLock.RUnlock()
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
pushUpdate(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushUpdate(option *Option) {
|
||||||
|
r, err := option.Export()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to export option to push update: %s", err)
|
||||||
|
} else {
|
||||||
|
dbController.PushUpdate(r)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,14 +2,41 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func parseAndSetConfig(jsonData string) error {
|
||||||
|
m, err := JSONToMap([]byte(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return setConfig(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAndSetDefaultConfig(jsonData string) error {
|
||||||
|
m, err := JSONToMap([]byte(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetDefaultConfig(m)
|
||||||
|
}
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
func TestGet(t *testing.T) {
|
||||||
|
|
||||||
err := SetConfig(`
|
err := log.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = parseAndSetConfig(`
|
||||||
{
|
{
|
||||||
"monkey": "1",
|
"monkey": "1",
|
||||||
"zebra": ["black", "white"],
|
"zebras": {
|
||||||
|
"zebra": ["black", "white"]
|
||||||
|
},
|
||||||
"elephant": 2,
|
"elephant": 2,
|
||||||
"hot": true,
|
"hot": true,
|
||||||
"cold": false
|
"cold": false
|
||||||
|
@ -19,7 +46,7 @@ func TestGet(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = SetDefaultConfig(`
|
err = parseAndSetDefaultConfig(`
|
||||||
{
|
{
|
||||||
"monkey": "0",
|
"monkey": "0",
|
||||||
"snake": "0",
|
"snake": "0",
|
||||||
|
@ -35,7 +62,7 @@ func TestGet(t *testing.T) {
|
||||||
t.Fatalf("monkey should be 1, is %s", monkey())
|
t.Fatalf("monkey should be 1, is %s", monkey())
|
||||||
}
|
}
|
||||||
|
|
||||||
zebra := GetAsStringArray("zebra", []string{})
|
zebra := GetAsStringArray("zebras/zebra", []string{})
|
||||||
if len(zebra()) != 2 || zebra()[0] != "black" || zebra()[1] != "white" {
|
if len(zebra()) != 2 || zebra()[0] != "black" || zebra()[1] != "white" {
|
||||||
t.Fatalf("zebra should be [\"black\", \"white\"], is %v", zebra())
|
t.Fatalf("zebra should be [\"black\", \"white\"], is %v", zebra())
|
||||||
}
|
}
|
||||||
|
@ -55,7 +82,7 @@ func TestGet(t *testing.T) {
|
||||||
t.Fatalf("cold should be false, is %v", cold())
|
t.Fatalf("cold should be false, is %v", cold())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = SetConfig(`
|
err = parseAndSetConfig(`
|
||||||
{
|
{
|
||||||
"monkey": "3"
|
"monkey": "3"
|
||||||
}
|
}
|
||||||
|
@ -79,7 +106,7 @@ func TestGet(t *testing.T) {
|
||||||
|
|
||||||
func BenchmarkGetAsStringCached(b *testing.B) {
|
func BenchmarkGetAsStringCached(b *testing.B) {
|
||||||
// Setup
|
// Setup
|
||||||
err := SetConfig(`
|
err := parseAndSetConfig(`
|
||||||
{
|
{
|
||||||
"monkey": "banana"
|
"monkey": "banana"
|
||||||
}
|
}
|
||||||
|
@ -100,7 +127,7 @@ func BenchmarkGetAsStringCached(b *testing.B) {
|
||||||
|
|
||||||
func BenchmarkGetAsStringRefetch(b *testing.B) {
|
func BenchmarkGetAsStringRefetch(b *testing.B) {
|
||||||
// Setup
|
// Setup
|
||||||
err := SetConfig(`
|
err := parseAndSetConfig(`
|
||||||
{
|
{
|
||||||
"monkey": "banana"
|
"monkey": "banana"
|
||||||
}
|
}
|
||||||
|
@ -120,7 +147,7 @@ func BenchmarkGetAsStringRefetch(b *testing.B) {
|
||||||
|
|
||||||
func BenchmarkGetAsIntCached(b *testing.B) {
|
func BenchmarkGetAsIntCached(b *testing.B) {
|
||||||
// Setup
|
// Setup
|
||||||
err := SetConfig(`
|
err := parseAndSetConfig(`
|
||||||
{
|
{
|
||||||
"monkey": 1
|
"monkey": 1
|
||||||
}
|
}
|
||||||
|
@ -141,7 +168,7 @@ func BenchmarkGetAsIntCached(b *testing.B) {
|
||||||
|
|
||||||
func BenchmarkGetAsIntRefetch(b *testing.B) {
|
func BenchmarkGetAsIntRefetch(b *testing.B) {
|
||||||
// Setup
|
// Setup
|
||||||
err := SetConfig(`
|
err := parseAndSetConfig(`
|
||||||
{
|
{
|
||||||
"monkey": 1
|
"monkey": 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
package integration
|
|
||||||
|
|
||||||
// API
|
|
|
@ -1,3 +0,0 @@
|
||||||
package integration
|
|
||||||
|
|
||||||
// register as module
|
|
|
@ -1,4 +0,0 @@
|
||||||
package integration
|
|
||||||
|
|
||||||
// persist config file
|
|
||||||
// create callback function in config to get updates
|
|
209
config/layers.go
209
config/layers.go
|
@ -5,15 +5,14 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/Safing/portbase/log"
|
||||||
"github.com/tidwall/sjson"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
configLock sync.RWMutex
|
configLock sync.RWMutex
|
||||||
|
|
||||||
userConfig = ""
|
userConfig = make(map[string]interface{})
|
||||||
defaultConfig = ""
|
defaultConfig = make(map[string]interface{})
|
||||||
|
|
||||||
// ErrInvalidJSON is returned by SetConfig and SetDefaultConfig if they receive invalid json.
|
// ErrInvalidJSON is returned by SetConfig and SetDefaultConfig if they receive invalid json.
|
||||||
ErrInvalidJSON = errors.New("json string invalid")
|
ErrInvalidJSON = errors.New("json string invalid")
|
||||||
|
@ -22,112 +21,109 @@ var (
|
||||||
ErrInvalidOptionType = errors.New("invalid option value type")
|
ErrInvalidOptionType = errors.New("invalid option value type")
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetConfig sets the (prioritized) user defined config.
|
// setConfig sets the (prioritized) user defined config.
|
||||||
func SetConfig(json string) error {
|
func setConfig(m map[string]interface{}) error {
|
||||||
if !gjson.Valid(json) {
|
|
||||||
return ErrInvalidJSON
|
|
||||||
}
|
|
||||||
|
|
||||||
configLock.Lock()
|
configLock.Lock()
|
||||||
defer configLock.Unlock()
|
defer configLock.Unlock()
|
||||||
userConfig = json
|
userConfig = m
|
||||||
resetValidityFlag()
|
resetValidityFlag()
|
||||||
|
|
||||||
|
go pushFullUpdate()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaultConfig sets the (fallback) default config.
|
// SetDefaultConfig sets the (fallback) default config.
|
||||||
func SetDefaultConfig(json string) error {
|
func SetDefaultConfig(m map[string]interface{}) error {
|
||||||
if !gjson.Valid(json) {
|
|
||||||
return ErrInvalidJSON
|
|
||||||
}
|
|
||||||
|
|
||||||
configLock.Lock()
|
configLock.Lock()
|
||||||
defer configLock.Unlock()
|
defer configLock.Unlock()
|
||||||
defaultConfig = json
|
defaultConfig = m
|
||||||
resetValidityFlag()
|
resetValidityFlag()
|
||||||
|
|
||||||
|
go pushFullUpdate()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateValue(name string, value interface{}) error {
|
func validateValue(name string, value interface{}) (*Option, error) {
|
||||||
optionsLock.RLock()
|
optionsLock.RLock()
|
||||||
defer optionsLock.RUnlock()
|
defer optionsLock.RUnlock()
|
||||||
|
|
||||||
option, ok := options[name]
|
option, ok := options[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
switch value.(type) {
|
return nil, errors.New("config option does not exist")
|
||||||
case string:
|
|
||||||
return nil
|
|
||||||
case []string:
|
|
||||||
return nil
|
|
||||||
case int:
|
|
||||||
return nil
|
|
||||||
case bool:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return ErrInvalidOptionType
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case string:
|
case string:
|
||||||
if option.OptType != OptTypeString {
|
if option.OptType != OptTypeString {
|
||||||
return fmt.Errorf("expected type string for option %s, got type %T", name, v)
|
return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), name, v)
|
||||||
}
|
}
|
||||||
if option.compiledRegex != nil {
|
if option.compiledRegex != nil {
|
||||||
if !option.compiledRegex.MatchString(v) {
|
if !option.compiledRegex.MatchString(v) {
|
||||||
return fmt.Errorf("validation failed: string \"%s\" did not match regex for option %s", v, name)
|
return nil, fmt.Errorf("validation failed: string \"%s\" did not match regex for option %s", v, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return option, nil
|
||||||
case []string:
|
case []string:
|
||||||
if option.OptType != OptTypeStringArray {
|
if option.OptType != OptTypeStringArray {
|
||||||
return fmt.Errorf("expected type string for option %s, got type %T", name, v)
|
return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), name, v)
|
||||||
}
|
}
|
||||||
if option.compiledRegex != nil {
|
if option.compiledRegex != nil {
|
||||||
for pos, entry := range v {
|
for pos, entry := range v {
|
||||||
if !option.compiledRegex.MatchString(entry) {
|
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, fmt.Errorf("validation failed: string \"%s\" at index %d did not match regex for option %s", entry, pos, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return option, nil
|
||||||
case int:
|
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||||
if option.OptType != OptTypeInt {
|
if option.OptType != OptTypeInt {
|
||||||
return fmt.Errorf("expected type int for option %s, got type %T", name, v)
|
return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), name, v)
|
||||||
}
|
}
|
||||||
return nil
|
if option.compiledRegex != nil {
|
||||||
|
if !option.compiledRegex.MatchString(fmt.Sprintf("%d", v)) {
|
||||||
|
return nil, fmt.Errorf("validation failed: number \"%d\" did not match regex for option %s", v, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return option, nil
|
||||||
case bool:
|
case bool:
|
||||||
if option.OptType != OptTypeBool {
|
if option.OptType != OptTypeBool {
|
||||||
return fmt.Errorf("expected type bool for option %s, got type %T", name, v)
|
return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), name, v)
|
||||||
}
|
}
|
||||||
return nil
|
return option, nil
|
||||||
default:
|
default:
|
||||||
return ErrInvalidOptionType
|
return nil, fmt.Errorf("invalid option value type: %T", value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfigOption sets a single value in the (prioritized) user defined config.
|
// SetConfigOption sets a single value in the (prioritized) user defined config.
|
||||||
func SetConfigOption(name string, value interface{}) error {
|
func SetConfigOption(name string, value interface{}) error {
|
||||||
|
return setConfigOption(name, value, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConfigOption(name string, value interface{}, push bool) error {
|
||||||
configLock.Lock()
|
configLock.Lock()
|
||||||
defer configLock.Unlock()
|
defer configLock.Unlock()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var newConfig string
|
|
||||||
|
|
||||||
if value == nil {
|
if value == nil {
|
||||||
newConfig, err = sjson.Delete(userConfig, name)
|
delete(userConfig, name)
|
||||||
} else {
|
} else {
|
||||||
err = validateValue(name, value)
|
var option *Option
|
||||||
|
option, err = validateValue(name, value)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
newConfig, err = sjson.Set(userConfig, name, value)
|
userConfig[name] = value
|
||||||
|
if push {
|
||||||
|
go pushUpdate(option)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
userConfig = newConfig
|
|
||||||
resetValidityFlag()
|
resetValidityFlag()
|
||||||
|
go saveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
@ -135,23 +131,29 @@ func SetConfigOption(name string, value interface{}) error {
|
||||||
|
|
||||||
// SetDefaultConfigOption sets a single value in the (fallback) default config.
|
// SetDefaultConfigOption sets a single value in the (fallback) default config.
|
||||||
func SetDefaultConfigOption(name string, value interface{}) error {
|
func SetDefaultConfigOption(name string, value interface{}) error {
|
||||||
|
return setDefaultConfigOption(name, value, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDefaultConfigOption(name string, value interface{}, push bool) error {
|
||||||
configLock.Lock()
|
configLock.Lock()
|
||||||
defer configLock.Unlock()
|
defer configLock.Unlock()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var newConfig string
|
|
||||||
|
|
||||||
if value == nil {
|
if value == nil {
|
||||||
newConfig, err = sjson.Delete(defaultConfig, name)
|
delete(defaultConfig, name)
|
||||||
} else {
|
} else {
|
||||||
err = validateValue(name, value)
|
var option *Option
|
||||||
|
option, err = validateValue(name, value)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
newConfig, err = sjson.Set(defaultConfig, name, value)
|
defaultConfig[name] = value
|
||||||
|
if push {
|
||||||
|
go pushUpdate(option)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defaultConfig = newConfig
|
|
||||||
resetValidityFlag()
|
resetValidityFlag()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,72 +161,113 @@ func SetDefaultConfigOption(name string, value interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// findValue find the correct value in the user or default config.
|
// findValue find the correct value in the user or default config.
|
||||||
func findValue(name string) (result gjson.Result) {
|
func findValue(name string) (result interface{}) {
|
||||||
configLock.RLock()
|
configLock.RLock()
|
||||||
defer configLock.RUnlock()
|
defer configLock.RUnlock()
|
||||||
|
|
||||||
result = gjson.Get(userConfig, name)
|
result, ok := userConfig[name]
|
||||||
if !result.Exists() {
|
if ok {
|
||||||
result = gjson.Get(defaultConfig, name)
|
return
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
result, ok = defaultConfig[name]
|
||||||
|
if ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsLock.RLock()
|
||||||
|
defer optionsLock.RUnlock()
|
||||||
|
|
||||||
|
option, ok := options[name]
|
||||||
|
if ok {
|
||||||
|
return option.DefaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Errorf("config: request for unregistered option: %s", name)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// findStringValue validates and returns the value with the given name.
|
// findStringValue validates and returns the value with the given name.
|
||||||
func findStringValue(name string, fallback string) (value string) {
|
func findStringValue(name string, fallback string) (value string) {
|
||||||
result := findValue(name)
|
result := findValue(name)
|
||||||
if !result.Exists() {
|
if result == nil {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
if result.Type != gjson.String {
|
v, ok := result.(string)
|
||||||
return fallback
|
if ok {
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
return result.String()
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// findStringArrayValue validates and returns the value with the given name.
|
// findStringArrayValue validates and returns the value with the given name.
|
||||||
func findStringArrayValue(name string, fallback []string) (value []string) {
|
func findStringArrayValue(name string, fallback []string) (value []string) {
|
||||||
result := findValue(name)
|
result := findValue(name)
|
||||||
if !result.Exists() {
|
if result == nil {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
if !result.IsArray() {
|
|
||||||
return fallback
|
v, ok := result.([]interface{})
|
||||||
}
|
if ok {
|
||||||
results := result.Array()
|
new := make([]string, len(v))
|
||||||
for _, r := range results {
|
for i, val := range v {
|
||||||
if r.Type != gjson.String {
|
s, ok := val.(string)
|
||||||
return fallback
|
if ok {
|
||||||
|
new[i] = s
|
||||||
|
} else {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
value = append(value, r.String())
|
return new
|
||||||
}
|
}
|
||||||
return value
|
|
||||||
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// findIntValue validates and returns the value with the given name.
|
// findIntValue validates and returns the value with the given name.
|
||||||
func findIntValue(name string, fallback int64) (value int64) {
|
func findIntValue(name string, fallback int64) (value int64) {
|
||||||
result := findValue(name)
|
result := findValue(name)
|
||||||
if !result.Exists() {
|
if result == nil {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
if result.Type != gjson.Number {
|
switch v := result.(type) {
|
||||||
return fallback
|
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 result.Int()
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// findBoolValue validates and returns the value with the given name.
|
// findBoolValue validates and returns the value with the given name.
|
||||||
func findBoolValue(name string, fallback bool) (value bool) {
|
func findBoolValue(name string, fallback bool) (value bool) {
|
||||||
result := findValue(name)
|
result := findValue(name)
|
||||||
if !result.Exists() {
|
if result == nil {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
switch result.Type {
|
v, ok := result.(bool)
|
||||||
case gjson.True:
|
if ok {
|
||||||
return true
|
return v
|
||||||
case gjson.False:
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return fallback
|
|
||||||
}
|
}
|
||||||
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,27 +4,26 @@ import "testing"
|
||||||
|
|
||||||
func TestLayersGetters(t *testing.T) {
|
func TestLayersGetters(t *testing.T) {
|
||||||
|
|
||||||
err := SetConfig("{invalid json")
|
mapData, err := JSONToMap([]byte(`
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = SetDefaultConfig("{invalid json")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = SetConfig(`
|
|
||||||
{
|
{
|
||||||
"monkey": "1",
|
"monkey": "1",
|
||||||
"zebra": ["black", "white"],
|
|
||||||
"weird_zebra": ["black", -1],
|
|
||||||
"elephant": 2,
|
"elephant": 2,
|
||||||
"hot": true
|
"zebras": {
|
||||||
|
"zebra": ["black", "white"],
|
||||||
|
"weird_zebra": ["black", -1]
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"hot": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`)
|
`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setConfig(mapData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test missing values
|
// Test missing values
|
||||||
|
@ -61,7 +60,7 @@ func TestLayersGetters(t *testing.T) {
|
||||||
t.Error("expected fallback value: [fallback]")
|
t.Error("expected fallback value: [fallback]")
|
||||||
}
|
}
|
||||||
|
|
||||||
mixedStringArray := GetAsStringArray("weird_zebra", []string{"fallback"})
|
mixedStringArray := GetAsStringArray("zebras/weird_zebra", []string{"fallback"})
|
||||||
if len(mixedStringArray()) != 1 || mixedStringArray()[0] != "fallback" {
|
if len(mixedStringArray()) != 1 || mixedStringArray()[0] != "fallback" {
|
||||||
t.Error("expected fallback value: [fallback]")
|
t.Error("expected fallback value: [fallback]")
|
||||||
}
|
}
|
||||||
|
@ -91,7 +90,7 @@ func TestLayersSetters(t *testing.T) {
|
||||||
})
|
})
|
||||||
Register(&Option{
|
Register(&Option{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Key: "zebra",
|
Key: "zebras/zebra",
|
||||||
Description: "description",
|
Description: "description",
|
||||||
ExpertiseLevel: 1,
|
ExpertiseLevel: 1,
|
||||||
OptType: OptTypeStringArray,
|
OptType: OptTypeStringArray,
|
||||||
|
@ -121,7 +120,7 @@ func TestLayersSetters(t *testing.T) {
|
||||||
if err := SetConfigOption("monkey", "banana"); err != nil {
|
if err := SetConfigOption("monkey", "banana"); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
if err := SetConfigOption("zebra", []string{"black", "white"}); err != nil {
|
if err := SetConfigOption("zebras/zebra", []string{"black", "white"}); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
if err := SetDefaultConfigOption("elephant", 2); err != nil {
|
if err := SetDefaultConfigOption("elephant", 2); err != nil {
|
||||||
|
@ -135,7 +134,7 @@ func TestLayersSetters(t *testing.T) {
|
||||||
if err := SetConfigOption("monkey", []string{"black", "white"}); err == nil {
|
if err := SetConfigOption("monkey", []string{"black", "white"}); err == nil {
|
||||||
t.Error("should fail")
|
t.Error("should fail")
|
||||||
}
|
}
|
||||||
if err := SetConfigOption("zebra", 2); err == nil {
|
if err := SetConfigOption("zebras/zebra", 2); err == nil {
|
||||||
t.Error("should fail")
|
t.Error("should fail")
|
||||||
}
|
}
|
||||||
if err := SetDefaultConfigOption("elephant", true); err == nil {
|
if err := SetDefaultConfigOption("elephant", true); err == nil {
|
||||||
|
@ -152,25 +151,25 @@ func TestLayersSetters(t *testing.T) {
|
||||||
if err := SetConfigOption("monkey", "dirt"); err == nil {
|
if err := SetConfigOption("monkey", "dirt"); err == nil {
|
||||||
t.Error("should fail")
|
t.Error("should fail")
|
||||||
}
|
}
|
||||||
if err := SetConfigOption("zebra", []string{"Element649"}); err == nil {
|
if err := SetConfigOption("zebras/zebra", []string{"Element649"}); err == nil {
|
||||||
t.Error("should fail")
|
t.Error("should fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
// unregistered checking
|
// unregistered checking
|
||||||
if err := SetConfigOption("invalid", "banana"); err != nil {
|
if err := SetConfigOption("invalid", "banana"); err == nil {
|
||||||
t.Error(err)
|
t.Error("should fail")
|
||||||
}
|
}
|
||||||
if err := SetConfigOption("invalid", []string{"black", "white"}); err != nil {
|
if err := SetConfigOption("invalid", []string{"black", "white"}); err == nil {
|
||||||
t.Error(err)
|
t.Error("should fail")
|
||||||
}
|
}
|
||||||
if err := SetConfigOption("invalid", 2); err != nil {
|
if err := SetConfigOption("invalid", 2); err == nil {
|
||||||
t.Error(err)
|
t.Error("should fail")
|
||||||
}
|
}
|
||||||
if err := SetConfigOption("invalid", true); err != nil {
|
if err := SetConfigOption("invalid", true); err == nil {
|
||||||
t.Error(err)
|
t.Error("should fail")
|
||||||
}
|
}
|
||||||
if err := SetConfigOption("invalid", []byte{0}); err != ErrInvalidOptionType {
|
if err := SetConfigOption("invalid", []byte{0}); err == nil {
|
||||||
t.Error("should fail with ErrInvalidOptionType")
|
t.Error("should fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete
|
// delete
|
||||||
|
|
32
config/main.go
Normal file
32
config/main.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/database"
|
||||||
|
"github.com/Safing/portbase/modules"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
modules.Register("config", prep, start, stop, "database")
|
||||||
|
}
|
||||||
|
|
||||||
|
func prep() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() error {
|
||||||
|
configFilePath = path.Join(database.GetDatabaseRoot(), "config.json")
|
||||||
|
|
||||||
|
err := loadConfig()
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return registerAsDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() error {
|
||||||
|
return nil
|
||||||
|
}
|
104
config/option.go
Normal file
104
config/option.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/tidwall/sjson"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/database/record"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Variable Type IDs for frontend Identification. Use ExternalOptType for extended types in the frontend.
|
||||||
|
const (
|
||||||
|
OptTypeString uint8 = 1
|
||||||
|
OptTypeStringArray uint8 = 2
|
||||||
|
OptTypeInt uint8 = 3
|
||||||
|
OptTypeBool uint8 = 4
|
||||||
|
|
||||||
|
ExpertiseLevelUser uint8 = 1
|
||||||
|
ExpertiseLevelExpert uint8 = 2
|
||||||
|
ExpertiseLevelDeveloper uint8 = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTypeName(t uint8) string {
|
||||||
|
switch t {
|
||||||
|
case OptTypeString:
|
||||||
|
return "string"
|
||||||
|
case OptTypeStringArray:
|
||||||
|
return "[]string"
|
||||||
|
case OptTypeInt:
|
||||||
|
return "int"
|
||||||
|
case OptTypeBool:
|
||||||
|
return "bool"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option describes a configuration option.
|
||||||
|
type Option struct {
|
||||||
|
Name string
|
||||||
|
Key string // category/sub/key
|
||||||
|
Description string
|
||||||
|
ExpertiseLevel uint8
|
||||||
|
OptType uint8
|
||||||
|
DefaultValue interface{}
|
||||||
|
ExternalOptType string
|
||||||
|
ValidationRegex string
|
||||||
|
compiledRegex *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export expors an option to a Record.
|
||||||
|
func (opt *Option) Export() (record.Record, error) {
|
||||||
|
data, err := json.Marshal(opt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configLock.RLock()
|
||||||
|
defer configLock.RUnlock()
|
||||||
|
|
||||||
|
userValue, ok := userConfig[opt.Key]
|
||||||
|
if ok {
|
||||||
|
data, err = sjson.SetBytes(data, "Value", userValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultValue, ok := defaultConfig[opt.Key]
|
||||||
|
if ok {
|
||||||
|
data, err = sjson.SetBytes(data, "DefaultValue", defaultValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := record.NewWrapper(fmt.Sprintf("config:%s", opt.Key), nil, record.JSON, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.SetMeta(&record.Meta{})
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortableOptions []*Option
|
||||||
|
|
||||||
|
// Len is the number of elements in the collection.
|
||||||
|
func (opts sortableOptions) Len() int {
|
||||||
|
return len(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less reports whether the element with
|
||||||
|
// index i should sort before the element with index j.
|
||||||
|
func (opts sortableOptions) Less(i, j int) bool {
|
||||||
|
return opts[i].Key < opts[j].Key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps the elements with indexes i and j.
|
||||||
|
func (opts sortableOptions) Swap(i, j int) {
|
||||||
|
opts[i], opts[j] = opts[j], opts[i]
|
||||||
|
}
|
125
config/persistence.go
Normal file
125
config/persistence.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configFilePath string
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadConfig() error {
|
||||||
|
data, err := ioutil.ReadFile(configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := JSONToMap(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return setConfig(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveConfig() (err error) {
|
||||||
|
data, err := MapToJSON(userConfig)
|
||||||
|
if err == nil {
|
||||||
|
err = ioutil.WriteFile(configFilePath, data, 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("config: failed to save config: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONToMap parses and flattens a hierarchical json object.
|
||||||
|
func JSONToMap(jsonData []byte) (map[string]interface{}, error) {
|
||||||
|
loaded := make(map[string]interface{})
|
||||||
|
err := json.Unmarshal(jsonData, &loaded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
flatten(loaded, loaded, "")
|
||||||
|
return loaded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func flatten(rootMap, subMap map[string]interface{}, subKey string) {
|
||||||
|
for key, entry := range subMap {
|
||||||
|
|
||||||
|
// get next level key
|
||||||
|
subbedKey := key
|
||||||
|
if subKey != "" {
|
||||||
|
subbedKey = fmt.Sprintf("%s/%s", subKey, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for next subMap
|
||||||
|
nextSub, ok := entry.(map[string]interface{})
|
||||||
|
if ok {
|
||||||
|
flatten(rootMap, nextSub, subbedKey)
|
||||||
|
delete(rootMap, key)
|
||||||
|
} else if subKey != "" {
|
||||||
|
// only set if not on root level
|
||||||
|
rootMap[subbedKey] = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapToJSON expands a flattened map and returns it as json.
|
||||||
|
func MapToJSON(mapData map[string]interface{}) ([]byte, error) {
|
||||||
|
configLock.RLock()
|
||||||
|
defer configLock.RUnlock()
|
||||||
|
|
||||||
|
new := make(map[string]interface{})
|
||||||
|
for key, value := range mapData {
|
||||||
|
new[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
expand(new)
|
||||||
|
return json.MarshalIndent(new, "", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// expand expands a flattened map.
|
||||||
|
func expand(mapData map[string]interface{}) {
|
||||||
|
var newMaps []map[string]interface{}
|
||||||
|
for key, entry := range mapData {
|
||||||
|
if strings.Contains(key, "/") {
|
||||||
|
parts := strings.SplitN(key, "/", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
|
||||||
|
// get subMap
|
||||||
|
var subMap map[string]interface{}
|
||||||
|
v, ok := mapData[parts[0]]
|
||||||
|
if ok {
|
||||||
|
subMap, ok = v.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
subMap = make(map[string]interface{})
|
||||||
|
newMaps = append(newMaps, subMap)
|
||||||
|
mapData[parts[0]] = subMap
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subMap = make(map[string]interface{})
|
||||||
|
newMaps = append(newMaps, subMap)
|
||||||
|
mapData[parts[0]] = subMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// set entry
|
||||||
|
subMap[parts[1]] = entry
|
||||||
|
// delete entry from
|
||||||
|
delete(mapData, key)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, entry := range newMaps {
|
||||||
|
expand(entry)
|
||||||
|
}
|
||||||
|
}
|
64
config/persistence_test.go
Normal file
64
config/persistence_test.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJSONMapConversion(t *testing.T) {
|
||||||
|
|
||||||
|
jsonData := `{
|
||||||
|
"a": "b",
|
||||||
|
"c": {
|
||||||
|
"d": "e",
|
||||||
|
"f": "g",
|
||||||
|
"h": {
|
||||||
|
"i": "j",
|
||||||
|
"k": "l",
|
||||||
|
"m": {
|
||||||
|
"n": "o"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"p": "q"
|
||||||
|
}`
|
||||||
|
jsonBytes := []byte(jsonData)
|
||||||
|
|
||||||
|
mapData := map[string]interface{}{
|
||||||
|
"a": "b",
|
||||||
|
"p": "q",
|
||||||
|
"c/d": "e",
|
||||||
|
"c/f": "g",
|
||||||
|
"c/h/i": "j",
|
||||||
|
"c/h/k": "l",
|
||||||
|
"c/h/m/n": "o",
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := JSONToMap(jsonBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := MapToJSON(mapData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(jsonBytes, j) {
|
||||||
|
t.Errorf("json does not match, got %s", j)
|
||||||
|
}
|
||||||
|
|
||||||
|
j2, err := MapToJSON(m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(jsonBytes, j2) {
|
||||||
|
t.Errorf("json does not match, got %s", j)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fails for some reason
|
||||||
|
// if !reflect.DeepEqual(mapData, m) {
|
||||||
|
// t.Errorf("maps do not match, got %s", m)
|
||||||
|
// }
|
||||||
|
}
|
|
@ -7,18 +7,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Variable Type IDs for frontend Identification. Values from 100 are free for custom use.
|
|
||||||
const (
|
|
||||||
OptTypeString uint8 = 1
|
|
||||||
OptTypeStringArray uint8 = 2
|
|
||||||
OptTypeInt uint8 = 3
|
|
||||||
OptTypeBool uint8 = 4
|
|
||||||
|
|
||||||
ExpertiseLevelUser uint8 = 1
|
|
||||||
ExpertiseLevelExpert uint8 = 2
|
|
||||||
ExpertiseLevelDeveloper uint8 = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
optionsLock sync.RWMutex
|
optionsLock sync.RWMutex
|
||||||
options = make(map[string]*Option)
|
options = make(map[string]*Option)
|
||||||
|
@ -27,18 +15,6 @@ var (
|
||||||
ErrIncompleteCall = errors.New("could not register config option: all fields, except for the validationRegex are mandatory")
|
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.
|
// Register registers a new configuration option.
|
||||||
func Register(option *Option) error {
|
func Register(option *Option) error {
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue