mirror of
https://github.com/safing/portbase
synced 2025-09-01 18:19:57 +00:00
Improve and expose config format handling
This commit is contained in:
parent
9d526314a9
commit
09e4c68f7b
3 changed files with 142 additions and 58 deletions
|
@ -2,8 +2,8 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
@ -72,70 +72,124 @@ func JSONToMap(jsonData []byte) (map[string]interface{}, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
flatten(loaded, loaded, "")
|
return Flatten(loaded), nil
|
||||||
return loaded, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func flatten(rootMap, subMap map[string]interface{}, subKey string) {
|
// Flatten returns a flattened copy of the given hierarchical config.
|
||||||
|
func Flatten(config map[string]interface{}) (flattenedConfig map[string]interface{}) {
|
||||||
|
flattenedConfig = make(map[string]interface{})
|
||||||
|
flattenMap(flattenedConfig, config, "")
|
||||||
|
return flattenedConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenMap(rootMap, subMap map[string]interface{}, subKey string) {
|
||||||
for key, entry := range subMap {
|
for key, entry := range subMap {
|
||||||
|
|
||||||
// get next level key
|
// get next level key
|
||||||
subbedKey := key
|
subbedKey := path.Join(subKey, key)
|
||||||
if subKey != "" {
|
|
||||||
subbedKey = fmt.Sprintf("%s/%s", subKey, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for next subMap
|
// check for next subMap
|
||||||
nextSub, ok := entry.(map[string]interface{})
|
nextSub, ok := entry.(map[string]interface{})
|
||||||
if ok {
|
if ok {
|
||||||
flatten(rootMap, nextSub, subbedKey)
|
flattenMap(rootMap, nextSub, subbedKey)
|
||||||
delete(rootMap, key)
|
} else {
|
||||||
} else if subKey != "" {
|
|
||||||
// only set if not on root level
|
// only set if not on root level
|
||||||
rootMap[subbedKey] = entry
|
rootMap[subbedKey] = entry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MapToJSON expands a flattened map and returns it as json. The map is altered in the process.
|
// MapToJSON expands a flattened map and returns it as json.
|
||||||
func MapToJSON(values map[string]interface{}) ([]byte, error) {
|
func MapToJSON(config map[string]interface{}) ([]byte, error) {
|
||||||
expand(values)
|
return json.MarshalIndent(Expand(config), "", " ")
|
||||||
return json.MarshalIndent(values, "", " ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// expand expands a flattened map.
|
// Expand returns a hierarchical copy of the given flattened config.
|
||||||
func expand(mapData map[string]interface{}) {
|
func Expand(flattenedConfig map[string]interface{}) (config map[string]interface{}) {
|
||||||
var newMaps []map[string]interface{}
|
config = make(map[string]interface{})
|
||||||
for key, entry := range mapData {
|
for key, entry := range flattenedConfig {
|
||||||
if strings.Contains(key, "/") {
|
PutValueIntoHierarchicalConfig(config, key, entry)
|
||||||
parts := strings.SplitN(key, "/", 2)
|
}
|
||||||
if len(parts) == 2 {
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
// get subMap
|
// PutValueIntoHierarchicalConfig injects a configuration entry into an hierarchical config map. Conflicting entries will be replaced.
|
||||||
var subMap map[string]interface{}
|
func PutValueIntoHierarchicalConfig(config map[string]interface{}, key string, value interface{}) {
|
||||||
v, ok := mapData[parts[0]]
|
parts := strings.Split(key, "/")
|
||||||
if ok {
|
|
||||||
subMap, ok = v.(map[string]interface{})
|
// create/check maps for all parts except the last one
|
||||||
|
subMap := config
|
||||||
|
for i := 0; i < len(parts)-1; i++ {
|
||||||
|
var nextSubMap map[string]interface{}
|
||||||
|
// get value
|
||||||
|
value, ok := subMap[parts[i]]
|
||||||
if !ok {
|
if !ok {
|
||||||
subMap = make(map[string]interface{})
|
// create new map and assign it
|
||||||
newMaps = append(newMaps, subMap)
|
nextSubMap = make(map[string]interface{})
|
||||||
mapData[parts[0]] = subMap
|
subMap[parts[i]] = nextSubMap
|
||||||
|
} else {
|
||||||
|
nextSubMap, ok = value.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
// create new map and assign it
|
||||||
|
nextSubMap = make(map[string]interface{})
|
||||||
|
subMap[parts[i]] = nextSubMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign for next parts loop
|
||||||
|
subMap = nextSubMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign value to last submap
|
||||||
|
subMap[parts[len(parts)-1]] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanFlattenedConfig removes all inexistent configuration options from the given flattened config map.
|
||||||
|
func CleanFlattenedConfig(flattenedConfig map[string]interface{}) {
|
||||||
|
optionsLock.RLock()
|
||||||
|
defer optionsLock.RUnlock()
|
||||||
|
|
||||||
|
for key := range flattenedConfig {
|
||||||
|
_, ok := options[key]
|
||||||
|
if !ok {
|
||||||
|
delete(flattenedConfig, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanHierarchicalConfig removes all inexistent configuration options from the given hierarchical config map.
|
||||||
|
func CleanHierarchicalConfig(config map[string]interface{}) {
|
||||||
|
optionsLock.RLock()
|
||||||
|
defer optionsLock.RUnlock()
|
||||||
|
|
||||||
|
cleanSubMap(config, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanSubMap(subMap map[string]interface{}, subKey string) (empty bool) {
|
||||||
|
var foundValid int
|
||||||
|
for key, value := range subMap {
|
||||||
|
value, ok := value.(map[string]interface{})
|
||||||
|
if ok {
|
||||||
|
// we found another section
|
||||||
|
isEmpty := cleanSubMap(value, path.Join(subKey, key))
|
||||||
|
if isEmpty {
|
||||||
|
delete(subMap, key)
|
||||||
|
} else {
|
||||||
|
foundValid++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
subMap = make(map[string]interface{})
|
// we found an option value
|
||||||
newMaps = append(newMaps, subMap)
|
if strings.Contains(key, "/") {
|
||||||
mapData[parts[0]] = subMap
|
delete(subMap, key)
|
||||||
}
|
} else {
|
||||||
|
_, ok := options[path.Join(subKey, key)]
|
||||||
// set entry
|
if ok {
|
||||||
subMap[parts[1]] = entry
|
foundValid++
|
||||||
// delete entry from
|
} else {
|
||||||
delete(mapData, key)
|
delete(subMap, key)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, entry := range newMaps {
|
|
||||||
expand(entry)
|
|
||||||
}
|
}
|
||||||
|
return foundValid == 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestJSONMapConversion(t *testing.T) {
|
var (
|
||||||
|
jsonData = `{
|
||||||
jsonData := `{
|
|
||||||
"a": "b",
|
"a": "b",
|
||||||
"c": {
|
"c": {
|
||||||
"d": "e",
|
"d": "e",
|
||||||
|
@ -22,9 +22,9 @@ func TestJSONMapConversion(t *testing.T) {
|
||||||
},
|
},
|
||||||
"p": "q"
|
"p": "q"
|
||||||
}`
|
}`
|
||||||
jsonBytes := []byte(jsonData)
|
jsonBytes = []byte(jsonData)
|
||||||
|
|
||||||
mapData := map[string]interface{}{
|
mapData = map[string]interface{}{
|
||||||
"a": "b",
|
"a": "b",
|
||||||
"p": "q",
|
"p": "q",
|
||||||
"c/d": "e",
|
"c/d": "e",
|
||||||
|
@ -33,32 +33,62 @@ func TestJSONMapConversion(t *testing.T) {
|
||||||
"c/h/k": "l",
|
"c/h/k": "l",
|
||||||
"c/h/m/n": "o",
|
"c/h/m/n": "o",
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
m, err := JSONToMap(jsonBytes)
|
func TestJSONMapConversion(t *testing.T) {
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// convert to json
|
||||||
j, err := MapToJSON(mapData)
|
j, err := MapToJSON(mapData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if to json matches
|
||||||
if !bytes.Equal(jsonBytes, j) {
|
if !bytes.Equal(jsonBytes, j) {
|
||||||
t.Errorf("json does not match, got %s", j)
|
t.Errorf("json does not match, got %s", j)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convert to map
|
||||||
|
m, err := JSONToMap(jsonBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// and back
|
||||||
j2, err := MapToJSON(m)
|
j2, err := MapToJSON(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if double convert matches
|
||||||
if !bytes.Equal(jsonBytes, j2) {
|
if !bytes.Equal(jsonBytes, j2) {
|
||||||
t.Errorf("json does not match, got %s", j)
|
t.Errorf("json does not match, got %s", j)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// fails for some reason
|
|
||||||
// if !reflect.DeepEqual(mapData, m) {
|
func TestConfigCleaning(t *testing.T) {
|
||||||
// t.Errorf("maps do not match, got %s", m)
|
// load
|
||||||
// }
|
configFlat, err := JSONToMap(jsonBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean everything
|
||||||
|
CleanFlattenedConfig(configFlat)
|
||||||
|
if len(configFlat) != 0 {
|
||||||
|
t.Errorf("should be empty: %+v", configFlat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load manuall for hierarchical config
|
||||||
|
configHier := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(jsonBytes, &configHier)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean everything
|
||||||
|
CleanHierarchicalConfig(configHier)
|
||||||
|
if len(configHier) != 0 {
|
||||||
|
t.Errorf("should be empty: %+v", configHier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ type perspectiveOption struct {
|
||||||
// NewPerspective parses the given config and returns it as a new perspective.
|
// NewPerspective parses the given config and returns it as a new perspective.
|
||||||
func NewPerspective(config map[string]interface{}) (*Perspective, error) {
|
func NewPerspective(config map[string]interface{}) (*Perspective, error) {
|
||||||
// flatten config structure
|
// flatten config structure
|
||||||
flatten(config, config, "")
|
config = Flatten(config)
|
||||||
|
|
||||||
perspective := &Perspective{
|
perspective := &Perspective{
|
||||||
config: make(map[string]*perspectiveOption),
|
config: make(map[string]*perspectiveOption),
|
||||||
|
|
Loading…
Add table
Reference in a new issue